Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-17120] account deprovisioning banner #13097

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,22 @@
</app-side-nav>

<ng-container *ngIf="organization$ | async as organization">
<bit-banner
*ngIf="showAccountDeprovisioningBanner$ | async"
(onClose)="bannerService.hideBanner(organization)"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
>
{{ "accountDeprovisioningNotification" | i18n }}
<a
href="https://bitwarden.com/help/claimed-accounts"
bitLink
linkType="contrast"
target="_blank"
rel="noreferrer"
>
{{ "learnMore" | i18n }}
</a>
</bit-banner>
<bit-banner
*ngIf="organization.isProviderUser"
[showClose]="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { combineLatest, filter, firstValueFrom, map, Observable, switchMap } from "rxjs";
import { combineLatest, filter, map, Observable, switchMap, withLatestFrom } from "rxjs";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
Expand All @@ -22,6 +22,7 @@
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 { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
Expand All @@ -32,6 +33,8 @@
import { WebLayoutModule } from "../../../layouts/web-layout.module";
import { AdminConsoleLogo } from "../../icons/admin-console-logo";

import { AccountDeprovisioningBannerService } from "./services/account-deprovisioning-banner.service";

@Component({
selector: "app-organization-layout",
templateUrl: "organization-layout.component.html",
Expand Down Expand Up @@ -61,26 +64,45 @@
organizationIsUnmanaged$: Observable<boolean>;
enterpriseOrganization$: Observable<boolean>;

showAccountDeprovisioningBanner$: Observable<boolean>;

constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
private configService: ConfigService,
private policyService: PolicyService,
private providerService: ProviderService,
protected bannerService: AccountDeprovisioningBannerService,
private accountService: AccountService,
) {}

async ngOnInit() {
document.body.classList.remove("layout_frontend");

const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.organization$ = this.route.params.pipe(
map((p) => p.organizationId),
switchMap((id) => this.organizationService.organizations$(userId).pipe(getById(id))),
withLatestFrom(this.accountService.activeAccount$.pipe(getUserId)),
switchMap(([orgId, userId]) =>
this.organizationService.organizations$(userId).pipe(getById(orgId)),

Check warning on line 87 in apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts#L87

Added line #L87 was not covered by tests
),
filter((org) => org != null),
);

this.showAccountDeprovisioningBanner$ = combineLatest([

Check warning on line 92 in apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts#L92

Added line #L92 was not covered by tests
this.bannerService.showBanner$,
this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioningBanner),
this.organization$,
]).pipe(
map(
([dismissedOrgs, featureFlagEnabled, organization]) =>
organization.productTierType === ProductTierType.Enterprise &&
organization.isAdmin &&
!dismissedOrgs?.includes(organization.id) &&
featureFlagEnabled,
),
);

this.canAccessExport$ = this.organization$.pipe(map((org) => org.canAccessExport));

this.showPaymentAndHistory$ = this.organization$.pipe(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { firstValueFrom } from "rxjs";

import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";

import { AccountDeprovisioningBannerService } from "./account-deprovisioning-banner.service";

describe("Account Deprovisioning Banner Service", () => {
const userId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
let bannerService: AccountDeprovisioningBannerService;

beforeEach(async () => {
accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
bannerService = new AccountDeprovisioningBannerService(stateProvider);
});

it("updates state with single org", async () => {
const fakeOrg = new Organization();
fakeOrg.id = "123";

await bannerService.hideBanner(fakeOrg);
const state = await firstValueFrom(bannerService.showBanner$);

expect(state).toEqual([fakeOrg.id]);
});

it("updates state with multiple orgs", async () => {
const fakeOrg1 = new Organization();
fakeOrg1.id = "123";
const fakeOrg2 = new Organization();
fakeOrg2.id = "234";
const fakeOrg3 = new Organization();
fakeOrg3.id = "987";

await bannerService.hideBanner(fakeOrg1);
await bannerService.hideBanner(fakeOrg2);
await bannerService.hideBanner(fakeOrg3);

const state = await firstValueFrom(bannerService.showBanner$);

expect(state).toContain(fakeOrg1.id);
expect(state).toContain(fakeOrg2.id);
expect(state).toContain(fakeOrg3.id);
});

it("does not add the same org id multiple times", async () => {
const fakeOrg = new Organization();
fakeOrg.id = "123";

await bannerService.hideBanner(fakeOrg);
await bannerService.hideBanner(fakeOrg);

const state = await firstValueFrom(bannerService.showBanner$);

expect(state).toEqual([fakeOrg.id]);
});

it("does not add null to the state", async () => {
await bannerService.hideBanner(null as unknown as Organization);
await bannerService.hideBanner(undefined as unknown as Organization);

const state = await firstValueFrom(bannerService.showBanner$);

expect(state).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Injectable } from "@angular/core";

import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import {
ACCOUNT_DEPROVISIONING_BANNER_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";

export const SHOW_BANNER_KEY = new UserKeyDefinition<string[]>(
ACCOUNT_DEPROVISIONING_BANNER_DISK,
"accountDeprovisioningBanner",
{
deserializer: (b) => b,

Check warning on line 14 in apps/web/src/app/admin-console/organizations/layouts/services/account-deprovisioning-banner.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/admin-console/organizations/layouts/services/account-deprovisioning-banner.service.ts#L14

Added line #L14 was not covered by tests
clearOn: [],
},
);

@Injectable({ providedIn: "root" })
export class AccountDeprovisioningBannerService {
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);

showBanner$ = this._showBanner.state$;

constructor(private stateProvider: StateProvider) {}

async hideBanner(organization: Organization) {
await this._showBanner.update((state) => {
if (!organization) {
return state;
}
if (!state) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add tests here. Abstracting this into a pure function will reduce test setup effort.

(state) => {
      if (!state) {
        state = [organization.id];
      } else if (!state.includes(organization.id)) {
        state.push(organization.id);
      }
      return state;
    }

return [organization.id];
} else if (!state.includes(organization.id)) {
return [...state, organization.id];
}
return state;
});
}
}
5 changes: 4 additions & 1 deletion apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1792,7 +1792,7 @@
},
"requestPending": {
"message": "Request pending"
},
},
"logBackInOthersToo": {
"message": "Please log back in. If you are using other Bitwarden applications log out and back in to those as well."
},
Expand Down Expand Up @@ -10285,5 +10285,8 @@
"example": "Acme c"
}
}
},
"accountDeprovisioningNotification" : {
"message": "Administrators now have the ability to delete member accounts that belong to a claimed domain."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,5 @@
></bit-nav-item>
</app-side-nav>

<bit-banner
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
(onClose)="(true)"
*ngIf="
(showProviderClientVaultPrivacyWarningBanner$ | async) &&
(providerClientVaultPrivacyBannerService.showBanner$ | async) != false
"
(onClose)="providerClientVaultPrivacyBannerService.hideBanner()"
>
{{ "providerClientVaultPrivacyNotification" | i18n }}
<a
href="https://bitwarden.com/contact/"
bitLink
linkType="secondary"
target="_blank"
rel="noreferrer"
>
{{ "contactBitwardenSupport" | i18n }} </a
>.
</bit-banner>
<router-outlet></router-outlet>
</app-layout>
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,15 @@
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BannerModule, IconModule, LinkModule } from "@bitwarden/components";
import { IconModule } from "@bitwarden/components";

Check warning on line 13 in bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts#L13

Added line #L13 was not covered by tests
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
import { WebLayoutModule } from "@bitwarden/web-vault/app/layouts/web-layout.module";

import { ProviderClientVaultPrivacyBannerService } from "./services/provider-client-vault-privacy-banner.service";

@Component({
selector: "providers-layout",
templateUrl: "providers-layout.component.html",
standalone: true,
imports: [
CommonModule,
RouterModule,
JslibModule,
WebLayoutModule,
IconModule,
LinkModule,
BannerModule,
],
imports: [CommonModule, RouterModule, JslibModule, WebLayoutModule, IconModule],
})
export class ProvidersLayoutComponent implements OnInit, OnDestroy {
protected readonly logo = ProviderPortalLogo;
Expand All @@ -41,15 +29,9 @@
protected isBillable: Observable<boolean>;
protected canAccessBilling$: Observable<boolean>;

protected showProviderClientVaultPrivacyWarningBanner$ = this.configService.getFeatureFlag$(
FeatureFlag.ProviderClientVaultPrivacyBanner,
);

constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
private configService: ConfigService,
protected providerClientVaultPrivacyBannerService: ProviderClientVaultPrivacyBannerService,
) {}

ngOnInit() {
Expand Down

This file was deleted.

2 changes: 2 additions & 0 deletions libs/common/src/enums/feature-flag.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export enum FeatureFlag {
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner",
NewDeviceVerification = "new-device-verification",
}

Expand Down Expand Up @@ -103,6 +104,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
[FeatureFlag.AccountDeprovisioningBanner]: FALSE,
[FeatureFlag.NewDeviceVerification]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;

Expand Down
10 changes: 7 additions & 3 deletions libs/common/src/platform/state/state-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition(
web: "disk-local",
},
);
export const AC_BANNERS_DISMISSED_DISK = new StateDefinition("acBannersDismissed", "disk", {
web: "disk-local",
});
export const ACCOUNT_DEPROVISIONING_BANNER_DISK = new StateDefinition(
"showAccountDeprovisioningBanner",
"disk",
{
web: "disk-local",
},
);

// Billing
export const BILLING_DISK = new StateDefinition("billing", "disk");
Expand Down
Loading