diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts
new file mode 100644
index 0000000000..75e63d4242
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts
@@ -0,0 +1,126 @@
+import { Component } from "@angular/core";
+import { TestBed } from "@angular/core/testing";
+import { provideRouter } from "@angular/router";
+import { RouterTestingHarness } from "@angular/router/testing";
+import { MockProxy, any, mock } from "jest-mock-extended";
+
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { ProductTierType } from "@bitwarden/common/billing/enums";
+import { DialogService } from "@bitwarden/components";
+
+import { isEnterpriseOrgGuard } from "./is-enterprise-org.guard";
+
+@Component({
+ template: "
This is the home screen!
",
+})
+export class HomescreenComponent {}
+
+@Component({
+ template: "This component can only be accessed by a enterprise organization!
",
+})
+export class IsEnterpriseOrganizationComponent {}
+
+@Component({
+ template: "This is the organization upgrade screen!
",
+})
+export class OrganizationUpgradeScreenComponent {}
+
+const orgFactory = (props: Partial = {}) =>
+ Object.assign(
+ new Organization(),
+ {
+ id: "myOrgId",
+ enabled: true,
+ type: OrganizationUserType.Admin,
+ },
+ props,
+ );
+
+describe("Is Enterprise Org Guard", () => {
+ let organizationService: MockProxy;
+ let dialogService: MockProxy;
+ let routerHarness: RouterTestingHarness;
+
+ beforeEach(async () => {
+ organizationService = mock();
+ dialogService = mock();
+
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: OrganizationService, useValue: organizationService },
+ { provide: DialogService, useValue: dialogService },
+ provideRouter([
+ {
+ path: "",
+ component: HomescreenComponent,
+ },
+ {
+ path: "organizations/:organizationId/enterpriseOrgsOnly",
+ component: IsEnterpriseOrganizationComponent,
+ canActivate: [isEnterpriseOrgGuard()],
+ },
+ {
+ path: "organizations/:organizationId/billing/subscription",
+ component: OrganizationUpgradeScreenComponent,
+ },
+ ]),
+ ],
+ });
+
+ routerHarness = await RouterTestingHarness.create();
+ });
+
+ it("redirects to `/` if the organization id provided is not found", async () => {
+ const org = orgFactory();
+ organizationService.get.calledWith(org.id).mockResolvedValue(null);
+ await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`);
+ expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
+ "This is the home screen!",
+ );
+ });
+
+ it.each([
+ ProductTierType.Free,
+ ProductTierType.Families,
+ ProductTierType.Teams,
+ ProductTierType.TeamsStarter,
+ ])(
+ "shows a dialog to users of a not enterprise organization and does not proceed with navigation for productTierType '%s'",
+ async (productTierType) => {
+ const org = orgFactory({
+ type: OrganizationUserType.User,
+ productTierType: productTierType,
+ });
+ organizationService.get.calledWith(org.id).mockResolvedValue(org);
+ await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`);
+ expect(dialogService.openSimpleDialog).toHaveBeenCalled();
+ expect(
+ routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "",
+ ).not.toBe("This component can only be accessed by a enterprise organization!");
+ },
+ );
+
+ it("redirects users with billing access to the billing screen to upgrade", async () => {
+ const org = orgFactory({
+ type: OrganizationUserType.Owner,
+ productTierType: ProductTierType.Teams,
+ });
+ organizationService.get.calledWith(org.id).mockResolvedValue(org);
+ dialogService.openSimpleDialog.calledWith(any()).mockResolvedValue(true);
+ await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`);
+ expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
+ "This is the organization upgrade screen!",
+ );
+ });
+
+ it("proceeds with navigation if the organization in question is a enterprise organization", async () => {
+ const org = orgFactory({ productTierType: ProductTierType.Enterprise });
+ organizationService.get.calledWith(org.id).mockResolvedValue(org);
+ await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`);
+ expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
+ "This component can only be accessed by a enterprise organization!",
+ );
+ });
+});
diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts
index 605ce0059d..3373f0cfd5 100644
--- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts
+++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts
@@ -1,44 +1,45 @@
-import { Injectable } from "@angular/core";
-import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
-import { firstValueFrom } from "rxjs";
+import { inject } from "@angular/core";
+import {
+ ActivatedRouteSnapshot,
+ CanActivateFn,
+ Router,
+ RouterStateSnapshot,
+} from "@angular/router";
+import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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 { DialogService } from "@bitwarden/components";
-@Injectable({
- providedIn: "root",
-})
-export class IsEnterpriseOrgGuard implements CanActivate {
- constructor(
- private router: Router,
- private organizationService: OrganizationService,
- private dialogService: DialogService,
- private configService: ConfigService,
- ) {}
+/**
+ * `CanActivateFn` that checks if the organization matching the id in the URL
+ * parameters is of enterprise type. If the organization is not enterprise instructions are
+ * provided on how to upgrade into an enterprise organization, and the user is redirected
+ * if they have access to upgrade the organization. If the organization is
+ * enterprise routing proceeds."
+ */
+export function isEnterpriseOrgGuard(): CanActivateFn {
+ return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
+ const router = inject(Router);
+ const organizationService = inject(OrganizationService);
+ const dialogService = inject(DialogService);
- async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
- const isMemberAccessReportEnabled = await firstValueFrom(
- this.configService.getFeatureFlag$(FeatureFlag.MemberAccessReport),
- );
-
- // TODO: Remove on "MemberAccessReport" feature flag cleanup
- if (!isMemberAccessReportEnabled) {
- return this.router.createUrlTree(["/"]);
- }
-
- const org = await this.organizationService.get(route.params.organizationId);
+ const org = await organizationService.get(route.params.organizationId);
if (org == null) {
- return this.router.createUrlTree(["/"]);
+ return router.createUrlTree(["/"]);
+ }
+
+ // TODO: Remove on "MemberAccessReport" feature flag cleanup
+ if (!canAccessFeature(FeatureFlag.MemberAccessReport)) {
+ return router.createUrlTree(["/"]);
}
if (org.productTierType != ProductTierType.Enterprise) {
// Users without billing permission can't access billing
if (!org.canEditSubscription) {
- await this.dialogService.openSimpleDialog({
+ await dialogService.openSimpleDialog({
title: { key: "upgradeOrganizationEnterprise" },
content: { key: "onlyAvailableForEnterpriseOrganization" },
acceptButtonText: { key: "ok" },
@@ -47,7 +48,7 @@ export class IsEnterpriseOrgGuard implements CanActivate {
});
return false;
} else {
- const upgradeConfirmed = await this.dialogService.openSimpleDialog({
+ const upgradeConfirmed = await dialogService.openSimpleDialog({
title: { key: "upgradeOrganizationEnterprise" },
content: { key: "onlyAvailableForEnterpriseOrganization" },
acceptButtonText: { key: "upgradeOrganization" },
@@ -55,7 +56,7 @@ export class IsEnterpriseOrgGuard implements CanActivate {
icon: "bwi-arrow-circle-up",
});
if (upgradeConfirmed) {
- await this.router.navigate(["organizations", org.id, "billing", "subscription"], {
+ await router.navigate(["organizations", org.id, "billing", "subscription"], {
queryParams: { upgrade: true, productTierType: ProductTierType.Enterprise },
});
}
@@ -63,5 +64,5 @@ export class IsEnterpriseOrgGuard implements CanActivate {
}
return org.productTierType == ProductTierType.Enterprise;
- }
+ };
}
diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts
index 1b31341e0d..413ced840d 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts
@@ -3,7 +3,7 @@ import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
-import { IsEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard";
+import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard";
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
@@ -72,7 +72,7 @@ const routes: Routes = [
data: {
titleId: "memberAccessReport",
},
- canActivate: [IsEnterpriseOrgGuard],
+ canActivate: [isEnterpriseOrgGuard()],
},
],
},