Skip to content

Commit

Permalink
[PM-13348] Browser Extension impacts on Free Bitwarden Family Policy (#…
Browse files Browse the repository at this point in the history
…12073)

* Add changes for enabled policy

* Remove unused property

* Refactor the changes

* remove duplicated across multiple components

* Add some test and documentations to service

* Correct the comment free family sponsorship for isExemptFromPolicy
  • Loading branch information
cyprain-okeke authored Nov 25, 2024
1 parent bb09121 commit c52eeb1
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 1 deletion.
83 changes: 83 additions & 0 deletions apps/browser/src/services/families-policy.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";

import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";

import { FamiliesPolicyService } from "./families-policy.service"; // Adjust the import as necessary

describe("FamiliesPolicyService", () => {
let service: FamiliesPolicyService;
let organizationService: MockProxy<OrganizationService>;
let policyService: MockProxy<PolicyService>;

beforeEach(() => {
organizationService = mock<OrganizationService>();
policyService = mock<PolicyService>();

TestBed.configureTestingModule({
providers: [
FamiliesPolicyService,
{ provide: OrganizationService, useValue: organizationService },
{ provide: PolicyService, useValue: policyService },
],
});

service = TestBed.inject(FamiliesPolicyService);
});

it("should return false when there are no enterprise organizations", async () => {
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(false));

const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$());
expect(result).toBe(false);
});

it("should return true when the policy is enabled for the one enterprise organization", async () => {
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true));

const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[];
organizationService.getAll$.mockReturnValue(of(organizations));

const policies = [{ organizationId: "org1", enabled: true }] as Policy[];
policyService.getAll$.mockReturnValue(of(policies));

const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$());
expect(result).toBe(true);
});

it("should return false when the policy is not enabled for the one enterprise organization", async () => {
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true));

const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[];
organizationService.getAll$.mockReturnValue(of(organizations));

const policies = [{ organizationId: "org1", enabled: false }] as Policy[];
policyService.getAll$.mockReturnValue(of(policies));

const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$());
expect(result).toBe(false);
});

it("should return true when there is exactly one enterprise organization that can manage sponsorships", async () => {
const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[];
organizationService.getAll$.mockReturnValue(of(organizations));

const result = await firstValueFrom(service.hasSingleEnterpriseOrg$());
expect(result).toBe(true);
});

it("should return false when there are multiple organizations that can manage sponsorships", async () => {
const organizations = [
{ id: "org1", canManageSponsorships: true },
{ id: "org2", canManageSponsorships: true },
] as Organization[];
organizationService.getAll$.mockReturnValue(of(organizations));

const result = await firstValueFrom(service.hasSingleEnterpriseOrg$());
expect(result).toBe(false);
});
});
54 changes: 54 additions & 0 deletions apps/browser/src/services/families-policy.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Injectable } from "@angular/core";
import { map, Observable, of, switchMap } from "rxjs";

import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";

@Injectable({ providedIn: "root" })
export class FamiliesPolicyService {
constructor(
private policyService: PolicyService,
private organizationService: OrganizationService,
) {}

hasSingleEnterpriseOrg$(): Observable<boolean> {
// Retrieve all organizations the user is part of
return this.organizationService.getAll$().pipe(
map((organizations) => {
// Filter to only those organizations that can manage sponsorships
const sponsorshipOrgs = organizations.filter((org) => org.canManageSponsorships);

// Check if there is exactly one organization that can manage sponsorships.
// This is important because users that are part of multiple organizations
// may always access free bitwarden family menu. We want to restrict access
// to the policy only when there is a single enterprise organization and the free family policy is turn.
return sponsorshipOrgs.length === 1;
}),
);
}

isFreeFamilyPolicyEnabled$(): Observable<boolean> {
return this.hasSingleEnterpriseOrg$().pipe(
switchMap((hasSingleEnterpriseOrg) => {
if (!hasSingleEnterpriseOrg) {
return of(false);
}
return this.organizationService.getAll$().pipe(
map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id),
switchMap((enterpriseOrgId) =>
this.policyService
.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy)
.pipe(
map(
(policies) =>
policies.find((policy) => policy.organizationId === enterpriseOrgId)?.enabled ??
false,
),
),
),
);
}),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
<bit-item *ngIf="familySponsorshipAvailable$ | async">
<bit-item
*ngIf="
(familySponsorshipAvailable$ | async) &&
!((isFreeFamilyPolicyEnabled$ | async) && (hasSingleEnterpriseOrg$ | async))
"
>
<button type="button" bit-item-content (click)="openFreeBitwardenFamiliesPage()">
{{ "freeBitwardenFamilies" | i18n }}
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { BrowserApi } from "../../../../platform/browser/browser-api";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { FamiliesPolicyService } from "../../../../services/families-policy.service";

@Component({
templateUrl: "more-from-bitwarden-page-v2.component.html",
Expand All @@ -30,15 +31,20 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
export class MoreFromBitwardenPageV2Component {
canAccessPremium$: Observable<boolean>;
protected familySponsorshipAvailable$: Observable<boolean>;
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
protected hasSingleEnterpriseOrg$: Observable<boolean>;

constructor(
private dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
private environmentService: EnvironmentService,
private organizationService: OrganizationService,
private familiesPolicyService: FamiliesPolicyService,
) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
}

async openFreeBitwardenFamiliesPage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ <h1 class="center">
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="openFreeBitwardenFamiliesPage()"
*ngIf="!((isFreeFamilyPolicyEnabled$ | async) && (hasSingleEnterpriseOrg$ | async))"
>
<div class="row-main">{{ "freeBitwardenFamilies" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DialogService } from "@bitwarden/components";

import { BrowserApi } from "../../../../platform/browser/browser-api";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { FamiliesPolicyService } from "../../../../services/families-policy.service";

@Component({
templateUrl: "more-from-bitwarden-page.component.html",
Expand All @@ -18,13 +19,18 @@ import { PopOutComponent } from "../../../../platform/popup/components/pop-out.c
})
export class MoreFromBitwardenPageComponent {
canAccessPremium$: Observable<boolean>;
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
protected hasSingleEnterpriseOrg$: Observable<boolean>;

constructor(
private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private environmentService: EnvironmentService,
private familiesPolicyService: FamiliesPolicyService,
) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
}

async openFreeBitwardenFamiliesPage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
case PolicyType.PersonalOwnership:
// individual vault policy applies to everyone except admins and owners
return organization.isAdmin;
case PolicyType.FreeFamiliesSponsorshipPolicy:
// free Bitwarden families policy applies to everyone
return false;
default:
return organization.canManagePolicies;
}
Expand Down

0 comments on commit c52eeb1

Please sign in to comment.