Skip to content

Commit

Permalink
implement vNextOrganization service for browser client
Browse files Browse the repository at this point in the history
  • Loading branch information
BTreston committed Jan 13, 2025
1 parent 67cea41 commit e910c59
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 60 deletions.
24 changes: 16 additions & 8 deletions apps/browser/src/services/families-policy.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,35 @@ 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 { vNextOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/vnext.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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";

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

describe("FamiliesPolicyService", () => {
let service: FamiliesPolicyService;
let organizationService: MockProxy<OrganizationService>;
let organizationService: MockProxy<vNextOrganizationService>;
let policyService: MockProxy<PolicyService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;

beforeEach(() => {
organizationService = mock<OrganizationService>();
organizationService = mock<vNextOrganizationService>();
policyService = mock<PolicyService>();
accountService = mockAccountServiceWith(userId);

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

Expand All @@ -40,7 +48,7 @@ describe("FamiliesPolicyService", () => {
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true));

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

const policies = [{ organizationId: "org1", enabled: true }] as Policy[];
policyService.getAll$.mockReturnValue(of(policies));
Expand All @@ -53,7 +61,7 @@ describe("FamiliesPolicyService", () => {
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true));

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

const policies = [{ organizationId: "org1", enabled: false }] as Policy[];
policyService.getAll$.mockReturnValue(of(policies));
Expand All @@ -64,7 +72,7 @@ describe("FamiliesPolicyService", () => {

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));
organizationService.organizations$.mockReturnValue(of(organizations));

const result = await firstValueFrom(service.hasSingleEnterpriseOrg$());
expect(result).toBe(true);
Expand All @@ -75,7 +83,7 @@ describe("FamiliesPolicyService", () => {
{ id: "org1", canManageSponsorships: true },
{ id: "org2", canManageSponsorships: true },
] as Organization[];
organizationService.getAll$.mockReturnValue(of(organizations));
organizationService.organizations$.mockReturnValue(of(organizations));

const result = await firstValueFrom(service.hasSingleEnterpriseOrg$());
expect(result).toBe(false);
Expand Down
57 changes: 34 additions & 23 deletions apps/browser/src/services/families-policy.service.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
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 { vNextOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/vnext.organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";

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

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);
return getUserId(this.accountService.activeAccount$).pipe(
switchMap((userId) =>
this.organizationService.organizations$(userId).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;
}),
// 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;
}),
),
),
);
}

Expand All @@ -34,18 +41,22 @@ export class FamiliesPolicyService {
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,
),
return getUserId(this.accountService.activeAccount$).pipe(
switchMap((userId) =>
this.organizationService.organizations$(userId).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,
),
),
),
),
),
);
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Observable, firstValueFrom } from "rxjs";
import { Observable, firstValueFrom, switchMap } from "rxjs";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { vNextOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/vnext.organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { DialogService, ItemModule } from "@bitwarden/components";
Expand Down Expand Up @@ -38,11 +40,14 @@ export class MoreFromBitwardenPageV2Component {
private dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
private environmentService: EnvironmentService,
private organizationService: OrganizationService,
private organizationService: vNextOrganizationService,
private accountService: AccountService,
private familiesPolicyService: FamiliesPolicyService,
) {
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe(
switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)),
);
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, of } from "rxjs";

import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { vNextOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/vnext.organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
Expand Down Expand Up @@ -51,7 +51,7 @@ describe("OpenAttachmentsComponent", () => {
} as Organization;

const getCipher = jest.fn().mockResolvedValue(cipherDomain);
const getOrganization = jest.fn().mockResolvedValue(org);
const organizations$ = jest.fn().mockReturnValue(of([org]));
const showFilePopoutMessage = jest.fn().mockReturnValue(false);

const mockUserId = Utils.newGuid() as UserId;
Expand All @@ -61,7 +61,7 @@ describe("OpenAttachmentsComponent", () => {
openCurrentPagePopout.mockClear();
getCipher.mockClear();
showToast.mockClear();
getOrganization.mockClear();
organizations$.mockClear();
showFilePopoutMessage.mockClear();

await TestBed.configureTestingModule({
Expand All @@ -81,8 +81,8 @@ describe("OpenAttachmentsComponent", () => {
useValue: { showToast },
},
{
provide: OrganizationService,
useValue: { get: getOrganization },
provide: vNextOrganizationService,
useValue: { organizations$ },
},
{
provide: FilePopoutUtilsService,
Expand Down Expand Up @@ -141,11 +141,11 @@ describe("OpenAttachmentsComponent", () => {

describe("Free Orgs", () => {
beforeEach(() => {
component.cipherIsAPartOfFreeOrg = undefined;
component.cipherIsAPartOfFreeOrg = false;
});

it("sets `cipherIsAPartOfFreeOrg` to false when the cipher is not a part of an organization", async () => {
cipherView.organizationId = null;
cipherView.organizationId = "";

await component.ngOnInit();

Expand All @@ -155,6 +155,7 @@ describe("OpenAttachmentsComponent", () => {
it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => {
cipherView.organizationId = "888-333-333";
org.productTierType = ProductTierType.Free;
org.id = cipherView.organizationId;

await component.ngOnInit();

Expand All @@ -164,6 +165,7 @@ describe("OpenAttachmentsComponent", () => {
it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => {
cipherView.organizationId = "888-333-333";
org.productTierType = ProductTierType.Families;
org.id = cipherView.organizationId;

await component.ngOnInit();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import { Router } from "@angular/router";
import { firstValueFrom, map } from "rxjs";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import {
getOrganizationById,
vNextOrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/vnext.organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
Expand Down Expand Up @@ -48,7 +52,7 @@ export class OpenAttachmentsComponent implements OnInit {
private router: Router,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private cipherService: CipherService,
private organizationService: OrganizationService,
private organizationService: vNextOrganizationService,
private toastService: ToastService,
private i18nService: I18nService,
private filePopoutUtilsService: FilePopoutUtilsService,
Expand Down Expand Up @@ -81,7 +85,12 @@ export class OpenAttachmentsComponent implements OnInit {
return;
}

const org = await this.organizationService.get(cipher.organizationId);
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const org = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(cipher.organizationId)),
);

this.cipherIsAPartOfFreeOrg = org.productTierType === ProductTierType.Free;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs";
import { filter } from "rxjs/operators";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { vNextOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/vnext.organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
Expand Down Expand Up @@ -83,12 +84,13 @@ export class ItemMoreOptionsComponent implements OnInit {
private i18nService: I18nService,
private vaultPopupAutofillService: VaultPopupAutofillService,
private accountService: AccountService,
private organizationService: OrganizationService,
private organizationService: vNextOrganizationService,
private cipherAuthorizationService: CipherAuthorizationService,
) {}

async ngOnInit(): Promise<void> {
this.hasOrganizations = await this.organizationService.hasOrganizations();
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.hasOrganizations = await firstValueFrom(this.organizationService.hasOrganizations(userId));
}

get canEdit() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { BehaviorSubject, Subject } from "rxjs";

import { CollectionService } from "@bitwarden/admin-console/common";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { vNextOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/vnext.organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
Expand Down Expand Up @@ -63,7 +63,7 @@ describe("VaultHeaderV2Component", () => {
},
{ provide: VaultSettingsService, useValue: mock<VaultSettingsService>() },
{ provide: FolderService, useValue: mock<FolderService>() },
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
{ provide: vNextOrganizationService, useValue: mock<vNextOrganizationService>() },
{ provide: CollectionService, useValue: mock<CollectionService>() },
{ provide: PolicyService, useValue: mock<PolicyService>() },
{ provide: SearchService, useValue: mock<SearchService>() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { Subject, firstValueFrom, from, Subscription } from "rxjs";
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";

import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { vNextOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/vnext.organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
Expand Down Expand Up @@ -70,7 +72,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
private searchService: SearchService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private passwordRepromptService: PasswordRepromptService,
private organizationService: OrganizationService,
private organizationService: vNextOrganizationService,
private accountService: AccountService,
private vaultFilterService: VaultFilterService,
private vaultSettingsService: VaultSettingsService,
) {}
Expand Down Expand Up @@ -272,7 +275,10 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
const dontShowIdentities = !(await firstValueFrom(
this.vaultSettingsService.showIdentitiesCurrentTab$,
));
this.showOrganizations = await this.organizationService.hasOrganizations();
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.showOrganizations = await firstValueFrom(
this.organizationService.hasOrganizations(userId),
);
if (!dontShowCards) {
otherTypes.push(CipherType.Card);
}
Expand Down
Loading

0 comments on commit e910c59

Please sign in to comment.