[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:
parent
bb0912154d
commit
c52eeb1cb3
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue