[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>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
<bit-item *ngIf="familySponsorshipAvailable$ | async">
|
<bit-item
|
||||||
|
*ngIf="
|
||||||
|
(familySponsorshipAvailable$ | async) &&
|
||||||
|
!((isFreeFamilyPolicyEnabled$ | async) && (hasSingleEnterpriseOrg$ | async))
|
||||||
|
"
|
||||||
|
>
|
||||||
<button type="button" bit-item-content (click)="openFreeBitwardenFamiliesPage()">
|
<button type="button" bit-item-content (click)="openFreeBitwardenFamiliesPage()">
|
||||||
{{ "freeBitwardenFamilies" | i18n }}
|
{{ "freeBitwardenFamilies" | i18n }}
|
||||||
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
|
<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 { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||||
|
import { FamiliesPolicyService } from "../../../../services/families-policy.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "more-from-bitwarden-page-v2.component.html",
|
templateUrl: "more-from-bitwarden-page-v2.component.html",
|
||||||
|
@ -30,15 +31,20 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
||||||
export class MoreFromBitwardenPageV2Component {
|
export class MoreFromBitwardenPageV2Component {
|
||||||
canAccessPremium$: Observable<boolean>;
|
canAccessPremium$: Observable<boolean>;
|
||||||
protected familySponsorshipAvailable$: Observable<boolean>;
|
protected familySponsorshipAvailable$: Observable<boolean>;
|
||||||
|
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
|
||||||
|
protected hasSingleEnterpriseOrg$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private familiesPolicyService: FamiliesPolicyService,
|
||||||
) {
|
) {
|
||||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||||
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
|
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
|
||||||
|
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
|
||||||
|
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
|
||||||
}
|
}
|
||||||
|
|
||||||
async openFreeBitwardenFamiliesPage() {
|
async openFreeBitwardenFamiliesPage() {
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
class="box-content-row box-content-row-flex text-default"
|
class="box-content-row box-content-row-flex text-default"
|
||||||
appStopClick
|
appStopClick
|
||||||
(click)="openFreeBitwardenFamiliesPage()"
|
(click)="openFreeBitwardenFamiliesPage()"
|
||||||
|
*ngIf="!((isFreeFamilyPolicyEnabled$ | async) && (hasSingleEnterpriseOrg$ | async))"
|
||||||
>
|
>
|
||||||
<div class="row-main">{{ "freeBitwardenFamilies" | i18n }}</div>
|
<div class="row-main">{{ "freeBitwardenFamilies" | i18n }}</div>
|
||||||
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<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 { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||||
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||||
|
import { FamiliesPolicyService } from "../../../../services/families-policy.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "more-from-bitwarden-page.component.html",
|
templateUrl: "more-from-bitwarden-page.component.html",
|
||||||
|
@ -18,13 +19,18 @@ import { PopOutComponent } from "../../../../platform/popup/components/pop-out.c
|
||||||
})
|
})
|
||||||
export class MoreFromBitwardenPageComponent {
|
export class MoreFromBitwardenPageComponent {
|
||||||
canAccessPremium$: Observable<boolean>;
|
canAccessPremium$: Observable<boolean>;
|
||||||
|
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
|
||||||
|
protected hasSingleEnterpriseOrg$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
|
private familiesPolicyService: FamiliesPolicyService,
|
||||||
) {
|
) {
|
||||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||||
|
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
|
||||||
|
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
|
||||||
}
|
}
|
||||||
|
|
||||||
async openFreeBitwardenFamiliesPage() {
|
async openFreeBitwardenFamiliesPage() {
|
||||||
|
|
|
@ -238,6 +238,9 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||||
case PolicyType.PersonalOwnership:
|
case PolicyType.PersonalOwnership:
|
||||||
// individual vault policy applies to everyone except admins and owners
|
// individual vault policy applies to everyone except admins and owners
|
||||||
return organization.isAdmin;
|
return organization.isAdmin;
|
||||||
|
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
||||||
|
// free Bitwarden families policy applies to everyone
|
||||||
|
return false;
|
||||||
default:
|
default:
|
||||||
return organization.canManagePolicies;
|
return organization.canManagePolicies;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue