[PM-13348] Browser Extension impacts on Free Bitwarden Family Policy (#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
This commit is contained in:
cyprain-okeke 2024-11-25 22:37:24 +01:00 committed by GitHub
parent bb0912154d
commit c52eeb1cb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 159 additions and 1 deletions

View File

@ -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);
});
});

View File

@ -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,
),
),
),
);
}),
);
}
}

View File

@ -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>

View File

@ -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",
@ -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() {

View File

@ -30,6 +30,7 @@
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>

View File

@ -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",
@ -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() {

View File

@ -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;
}