From a949f793ed8ee3dab99756146b87859920ff73f9 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 22 Jan 2025 15:20:25 -0500 Subject: [PATCH] [PM-15506] Implement vNextOrganizationService (#12839) * [PM-15506] Wire up vNextOrganizationService for libs/common and libs/angular (#12683) * Wire up vNextOrganizationService in PolicyService * Wire vNextOrganizationService in SyncService * wire vNextOrganizationService for EventCollectionService * wire vNextOrganizationService for KeyConnectorService * wire up vNextOrganizationService for CipherAuthorizationService * Wire up vNextOrganizationService in PolicyService * Wire vNextOrganizationService in SyncService * wire vNextOrganizationService for EventCollectionService * wire vNextOrganizationService for KeyConnectorService * wire up vNextOrganizationService for CipherAuthorizationService * wire vNextOrganizationService for share.component * wire vNextOrganizationService for collections.component * wire vNextOrganizationServcie for add-account-credit-dialog * wire vNextOrganizationService for vault-filter.service * fix browser errors for vNextOrganizationService implementation in libs * fix desktop errors for vNextOrganizationService implementation for libs * fix linter errors * fix CLI errors on vNextOrganizationServcie implementations for libs * [PM-15506] Wire up vNextOrganizationService for web client (#12810) PR to a feature branch, no need to review until this goes to main. * implement vNextOrganization service for browser client (#12844) PR to feature branch, no need for review yet. * wire vNextOrganizationService for licence and some web router guards * wire vNextOrganizationService in tests * remove vNext notation for OrganizationService and related * Merge branch 'main' into ac/pm-15506-vNextOrganizationService * fix tsstrict error * fix test, fix ts strict error --- .../browser/src/background/main.background.ts | 5 +- .../src/popup/services/services.module.ts | 4 +- .../services/families-policy.service.spec.ts | 16 +- .../src/services/families-policy.service.ts | 53 +-- .../more-from-bitwarden-page-v2.component.ts | 5 +- .../open-attachments.component.spec.ts | 12 +- .../open-attachments.component.ts | 13 +- .../item-more-options.component.ts | 4 +- .../vault-popup-items.service.spec.ts | 11 +- .../services/vault-popup-items.service.ts | 17 +- .../vault-popup-list-filters.service.spec.ts | 25 +- .../vault-popup-list-filters.service.ts | 4 +- .../vault/services/vault-filter.service.ts | 2 +- apps/cli/src/commands/get.command.ts | 13 +- apps/cli/src/commands/list.command.ts | 29 +- .../service-container/service-container.ts | 17 +- apps/cli/src/tools/import.command.ts | 15 +- apps/cli/src/vault.program.ts | 1 + apps/cli/src/vault/create.command.ts | 12 +- apps/desktop/src/app/app.component.ts | 4 +- .../guards/is-enterprise-org.guard.spec.ts | 19 +- .../guards/is-enterprise-org.guard.ts | 15 +- .../guards/is-paid-org.guard.spec.ts | 17 +- .../organizations/guards/is-paid-org.guard.ts | 15 +- .../guards/org-permissions.guard.spec.ts | 23 +- .../guards/org-permissions.guard.ts | 12 +- .../guards/org-redirect.guard.spec.ts | 19 +- .../guards/org-redirect.guard.ts | 21 +- .../integrations/integrations.component.ts | 17 +- .../layouts/organization-layout.component.ts | 8 +- .../organizations/manage/events.component.ts | 17 +- .../manage/group-add-edit.component.ts | 17 +- .../member-dialog/member-dialog.component.ts | 16 +- .../members/members.component.ts | 17 +- ...zation-user-reset-password.service.spec.ts | 8 +- ...rganization-user-reset-password.service.ts | 3 +- .../policies/master-password.component.ts | 16 +- .../policies/policies.component.ts | 20 +- .../policies/reset-password.component.ts | 26 +- .../reporting/reports-home.component.ts | 18 +- .../settings/account.component.ts | 26 +- .../delete-organization-dialog.component.ts | 15 +- .../settings/org-import.component.ts | 12 +- .../settings/two-factor-setup.component.ts | 14 +- ...families-for-enterprise-setup.component.ts | 23 +- .../exposed-passwords-report.component.ts | 18 +- .../inactive-two-factor-report.component.ts | 18 +- .../reused-passwords-report.component.ts | 18 +- .../unsecured-websites-report.component.ts | 18 +- .../tools/weak-passwords-report.component.ts | 18 +- apps/web/src/app/app.component.ts | 28 +- .../settings/account/account.component.ts | 15 +- .../settings/account/profile.component.ts | 16 +- .../settings/change-password.component.ts | 3 +- .../emergency-access.component.ts | 4 +- .../emergency-view-dialog.component.spec.ts | 1 + .../two-factor/two-factor-setup.component.ts | 2 +- .../guards/organization-is-unmanaged.guard.ts | 24 +- .../adjust-subscription.component.ts | 19 +- .../change-plan-dialog.component.ts | 18 +- .../organization-plans.component.ts | 20 +- ...ganization-subscription-cloud.component.ts | 15 +- ...ization-subscription-selfhost.component.ts | 15 +- .../organization-payment-method.component.ts | 18 +- .../sm-adjust-subscription.component.ts | 19 +- .../sm-subscribe-standalone.component.ts | 7 +- .../services/free-families-policy.service.ts | 30 +- .../settings/sponsored-families.component.ts | 17 +- .../shared/add-credit-dialog.component.ts | 14 +- .../shared/payment-method.component.ts | 18 +- .../org-switcher/org-switcher.component.ts | 21 +- .../navigation-switcher.stories.ts | 21 +- .../product-switcher.stories.ts | 26 +- .../shared/product-switcher.service.spec.ts | 74 ++-- .../shared/product-switcher.service.ts | 24 +- .../request-sm-access.component.ts | 7 +- .../sm-landing.component.ts | 13 +- .../reports/pages/cipher-report.component.ts | 8 +- ...exposed-passwords-report.component.spec.ts | 13 +- .../exposed-passwords-report.component.ts | 3 + ...active-two-factor-report.component.spec.ts | 13 +- .../inactive-two-factor-report.component.ts | 3 + .../reused-passwords-report.component.spec.ts | 14 +- .../reused-passwords-report.component.ts | 3 + ...nsecured-websites-report.component.spec.ts | 13 +- .../unsecured-websites-report.component.ts | 3 + .../weak-passwords-report.component.spec.ts | 13 +- .../pages/weak-passwords-report.component.ts | 3 + .../collection-dialog.component.ts | 18 +- .../add-edit-v2.component.spec.ts | 2 +- .../bulk-share-dialog.component.ts | 4 +- .../organization-options.component.ts | 27 +- .../services/vault-filter.service.spec.ts | 2 +- .../services/vault-filter.service.ts | 6 +- .../vault/individual-vault/vault.component.ts | 28 +- .../individual-vault/view.component.spec.ts | 12 +- .../vault/individual-vault/view.component.ts | 16 +- .../bulk-collections-dialog.component.ts | 15 +- ...console-cipher-form-config.service.spec.ts | 11 +- ...dmin-console-cipher-form-config.service.ts | 18 +- .../vault-header/vault-header.component.ts | 3 - .../app/vault/org-vault/vault.component.ts | 50 ++- .../device-approval/approve-all.command.ts | 16 +- .../device-approval/approve.command.ts | 22 +- .../device-approval/deny-all.command.ts | 18 +- .../device-approval/deny.command.ts | 17 +- .../device-approval/list.command.ts | 18 +- bitwarden_license/bit-common/jest.config.js | 14 +- bitwarden_license/bit-web/jest.config.js | 14 +- .../providers/clients/clients.component.ts | 10 +- .../bit-web/src/app/auth/sso/sso.component.ts | 17 +- .../guards/sm-org-enabled.guard.ts | 14 +- .../app/secrets-manager/guards/sm.guard.ts | 7 +- .../layout/navigation.component.ts | 18 +- .../overview/overview.component.ts | 18 +- .../guards/project-access.guard.spec.ts | 13 +- .../project/project-secrets.component.ts | 25 +- .../projects/project/project.component.ts | 18 +- .../projects/projects/projects.component.ts | 24 +- .../secrets/dialog/secret-dialog.component.ts | 21 +- .../secrets/secrets.component.ts | 17 +- .../service-account-access.guard.spec.ts | 13 +- .../service-accounts.component.ts | 17 +- .../settings/porting/sm-export.component.ts | 17 +- .../access-policy-selector.service.spec.ts | 50 +-- .../access-policy-selector.service.ts | 29 +- .../shared/new-menu.component.ts | 19 +- .../shared/org-suspended.component.ts | 19 +- .../all-applications.component.ts | 72 ++-- ...sword-health-members-uri.component.spec.ts | 6 + .../components/collections.component.ts | 10 +- .../add-account-credit-dialog.component.ts | 16 +- .../angular/src/components/share.component.ts | 6 +- .../src/services/jslib-services.module.ts | 7 +- .../vault/components/add-edit.component.ts | 28 +- .../services/vault-filter.service.ts | 13 +- .../organization.service.abstraction.ts | 52 +-- .../vnext.organization.service.ts | 111 ------ .../models/data/organization.data.spec.ts | 2 +- ...s => default-organization.service.spec.ts} | 8 +- ...ice.ts => default-organization.service.ts} | 8 +- .../organization/organization.service.spec.ts | 324 ------------------ .../organization/organization.service.ts | 160 --------- ...ization.state.ts => organization.state.ts} | 0 .../services/policy/policy.service.spec.ts | 8 +- .../services/policy/policy.service.ts | 10 +- .../services/key-connector.service.spec.ts | 18 +- .../auth/services/key-connector.service.ts | 6 +- .../src/platform/sync/core-sync.service.ts | 3 +- .../src/platform/sync/default-sync.service.ts | 5 +- .../event/event-collection.service.ts | 36 +- .../cipher-authorization.service.spec.ts | 59 ++-- .../services/cipher-authorization.service.ts | 15 +- .../src/components/import.component.ts | 28 +- .../export-scope-callout.component.ts | 18 +- .../src/components/export.component.ts | 40 ++- .../default-cipher-form-config.service.ts | 22 +- .../src/cipher-view/cipher-view.component.ts | 13 +- .../assign-collections.component.ts | 60 ++-- .../components/password-reprompt.component.ts | 5 +- .../services/default-task.service.spec.ts | 2 +- .../tasks/services/default-task.service.ts | 2 +- package-lock.json | 4 + 163 files changed, 1972 insertions(+), 1246 deletions(-) delete mode 100644 libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts rename libs/common/src/admin-console/services/organization/{default-vnext-organization.service.spec.ts => default-organization.service.spec.ts} (96%) rename libs/common/src/admin-console/services/organization/{default-vnext-organization.service.ts => default-organization.service.ts} (92%) delete mode 100644 libs/common/src/admin-console/services/organization/organization.service.spec.ts delete mode 100644 libs/common/src/admin-console/services/organization/organization.service.ts rename libs/common/src/admin-console/services/organization/{vnext-organization.state.ts => organization.state.ts} (100%) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3a60cc5210..b26360407e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -25,7 +25,7 @@ import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin- import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; @@ -669,7 +669,7 @@ export default class MainBackground { this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.organizationService = new OrganizationService(this.stateProvider); + this.organizationService = new DefaultOrganizationService(this.stateProvider); this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( @@ -1248,6 +1248,7 @@ export default class MainBackground { this.cipherAuthorizationService = new DefaultCipherAuthorizationService( this.collectionService, this.organizationService, + this.accountService, ); this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 24d82ab8b6..20cd1d05b0 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,8 +31,8 @@ import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarde import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -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 { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { AccountService, AccountService as AccountServiceAbstraction, @@ -369,7 +369,7 @@ const safeProviders: SafeProvider[] = [ provide: VaultFilterService, useClass: VaultFilterService, deps: [ - OrganizationService, + DefaultOrganizationService, FolderServiceAbstraction, CipherService, CollectionService, diff --git a/apps/browser/src/services/families-policy.service.spec.ts b/apps/browser/src/services/families-policy.service.spec.ts index 19291bcd82..65a861038b 100644 --- a/apps/browser/src/services/families-policy.service.spec.ts +++ b/apps/browser/src/services/families-policy.service.spec.ts @@ -6,6 +6,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/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 @@ -13,16 +17,20 @@ describe("FamiliesPolicyService", () => { let service: FamiliesPolicyService; let organizationService: MockProxy; let policyService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); policyService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ providers: [ FamiliesPolicyService, { provide: OrganizationService, useValue: organizationService }, { provide: PolicyService, useValue: policyService }, + { provide: AccountService, useValue: accountService }, ], }); @@ -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)); @@ -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)); @@ -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); @@ -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); diff --git a/apps/browser/src/services/families-policy.service.ts b/apps/browser/src/services/families-policy.service.ts index 426f39dcfd..887e883695 100644 --- a/apps/browser/src/services/families-policy.service.ts +++ b/apps/browser/src/services/families-policy.service.ts @@ -4,27 +4,34 @@ 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"; +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 accountService: AccountService, ) {} hasSingleEnterpriseOrg$(): Observable { // 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; + }), + ), + ), ); } @@ -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, + ), + ), ), + ), ), ); }), diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts index 8b880e8867..20e5548f99 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts @@ -6,6 +6,7 @@ import { Observable, firstValueFrom, of, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/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"; @@ -43,6 +44,9 @@ export class MoreFromBitwardenPageV2Component { private familiesPolicyService: FamiliesPolicyService, private accountService: AccountService, ) { + this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)), + ); this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => account @@ -50,7 +54,6 @@ export class MoreFromBitwardenPageV2Component { : of(false), ), ); - this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$; this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); } diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 4f6c4aa07c..66d9096cd5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -50,7 +50,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; @@ -67,7 +67,7 @@ describe("OpenAttachmentsComponent", () => { openCurrentPagePopout.mockClear(); getCipher.mockClear(); showToast.mockClear(); - getOrganization.mockClear(); + organizations$.mockClear(); showFilePopoutMessage.mockClear(); hasPremiumFromAnySource$.next(true); @@ -89,7 +89,7 @@ describe("OpenAttachmentsComponent", () => { }, { provide: OrganizationService, - useValue: { get: getOrganization }, + useValue: { organizations$ }, }, { provide: FilePopoutUtilsService, @@ -148,11 +148,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(); @@ -162,6 +162,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(); @@ -171,6 +172,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(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index 5e27ccd5c4..aca494716b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -7,8 +7,12 @@ import { Router } from "@angular/router"; import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/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"; @@ -86,7 +90,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; } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 5d3dee9018..8634d68005 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -9,6 +9,7 @@ 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 { 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"; @@ -88,7 +89,8 @@ export class ItemMoreOptionsComponent implements OnInit { ) {} async ngOnInit(): Promise { - this.hasOrganizations = await this.organizationService.hasOrganizations(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.hasOrganizations = await firstValueFrom(this.organizationService.hasOrganizations(userId)); } get canEdit() { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 528aae111c..7b241a6c10 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -6,10 +6,12 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { ObservableTracker } from "@bitwarden/common/spec"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -43,6 +45,8 @@ describe("VaultPopupItemsService", () => { const vaultAutofillServiceMock = mock(); const syncServiceMock = mock(); const inlineMenuFieldQualificationServiceMock = mock(); + const userId = Utils.newGuid() as UserId; + const accountServiceMock = mockAccountServiceWith(userId); beforeEach(() => { allCiphers = cipherFactory(10); @@ -99,7 +103,7 @@ describe("VaultPopupItemsService", () => { { id: "col2", name: "Collection 2" } as CollectionView, ]; - organizationServiceMock.organizations$ = new BehaviorSubject([mockOrg]); + organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg])); collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); activeUserLastSync$ = new BehaviorSubject(new Date()); @@ -111,6 +115,7 @@ describe("VaultPopupItemsService", () => { { provide: VaultSettingsService, useValue: vaultSettingsServiceMock }, { provide: SearchService, useValue: searchService }, { provide: OrganizationService, useValue: organizationServiceMock }, + { provide: AccountService, useValue: accountServiceMock }, { provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock }, { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index fb230df795..2afa5b9a14 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -24,6 +24,7 @@ import { 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; @@ -56,6 +57,9 @@ export class VaultPopupItemsService { latestSearchText$: Observable = this._searchText$.asObservable(); + private organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ); /** * Observable that contains the list of other cipher types that should be shown * in the autofill section of the Vault tab. Depends on vault settings. @@ -97,10 +101,7 @@ export class VaultPopupItemsService { private _activeCipherList$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([ - this.organizationService.organizations$, - this.collectionService.decryptedCollections$, - ]).pipe( + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -232,7 +233,7 @@ export class VaultPopupItemsService { /** Observable that indicates when the user should see the deactivated org state */ showDeactivatedOrg$: Observable = combineLatest([ this.vaultPopupListFiltersService.filters$.pipe(distinctUntilKeyChanged("organization")), - this.organizationService.organizations$, + this.organizations$, ]).pipe( map(([filters, orgs]) => { if (!filters.organization || filters.organization.id === MY_VAULT_ID) { @@ -249,10 +250,7 @@ export class VaultPopupItemsService { */ deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([ - this.organizationService.organizations$, - this.collectionService.decryptedCollections$, - ]).pipe( + combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -281,6 +279,7 @@ export class VaultPopupItemsService { private collectionService: CollectionService, private vaultPopupAutofillService: VaultPopupAutofillService, private syncService: SyncService, + private accountService: AccountService, ) {} applyFilter(newSearchText: string) { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index e1236be08f..7f570e8f5c 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -23,7 +23,9 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi describe("VaultPopupListFiltersService", () => { let service: VaultPopupListFiltersService; - const memberOrganizations$ = new BehaviorSubject([]); + const _memberOrganizations$ = new BehaviorSubject([]); + const memberOrganizations$ = (userId: UserId) => _memberOrganizations$; + const organizations$ = new BehaviorSubject([]); const folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); const decryptedCollections$ = new BehaviorSubject([]); @@ -44,6 +46,7 @@ describe("VaultPopupListFiltersService", () => { const organizationService = { memberOrganizations$, + organizations$, } as unknown as OrganizationService; const i18nService = { @@ -58,7 +61,7 @@ describe("VaultPopupListFiltersService", () => { const update = jest.fn().mockResolvedValue(undefined); beforeEach(() => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); decryptedCollections$.next([]); policyAppliesToActiveUser$.next(false); policyService.policyAppliesToActiveUser$.mockClear(); @@ -135,7 +138,7 @@ describe("VaultPopupListFiltersService", () => { describe("organizations$", () => { it('does not add "myVault" to the list of organizations when there are no organizations', (done) => { - memberOrganizations$.next([]); + _memberOrganizations$.next([]); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([]); @@ -145,7 +148,7 @@ describe("VaultPopupListFiltersService", () => { it('adds "myVault" to the list of organizations when there are other organizations', (done) => { const orgs = [{ name: "bobby's org", id: "1234-3323-23223" }] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual(["myVault", "bobby's org"]); @@ -158,7 +161,7 @@ describe("VaultPopupListFiltersService", () => { { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -179,7 +182,7 @@ describe("VaultPopupListFiltersService", () => { it("returns an empty array when the policy applies and there is a single organization", (done) => { policyAppliesToActiveUser$.next(true); - memberOrganizations$.next([ + _memberOrganizations$.next([ { name: "bobby's org", id: "1234-3323-23223" }, ] as Organization[]); @@ -196,7 +199,7 @@ describe("VaultPopupListFiltersService", () => { { name: "alice's org", id: "2223-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -216,7 +219,7 @@ describe("VaultPopupListFiltersService", () => { { name: "catherine's org", id: "77733-4343-99888" }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.label)).toEqual([ @@ -240,7 +243,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -258,7 +261,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual(["bwi-user", "bwi-family"]); @@ -276,7 +279,7 @@ describe("VaultPopupListFiltersService", () => { }, ] as Organization[]; - memberOrganizations$.next(orgs); + _memberOrganizations$.next(orgs); service.organizations$.subscribe((organizations) => { expect(organizations.map((o) => o.icon)).toEqual([ diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 8455fd587d..6190d14a6a 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -208,7 +208,9 @@ export class VaultPopupListFiltersService { * Organization array structured to be directly passed to `ChipSelectComponent` */ organizations$: Observable[]> = combineLatest([ - this.organizationService.memberOrganizations$, + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.memberOrganizations$(account?.id)), + ), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), ]).pipe( map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ diff --git a/apps/browser/src/vault/services/vault-filter.service.ts b/apps/browser/src/vault/services/vault-filter.service.ts index 305c7de487..f8b22f2f88 100644 --- a/apps/browser/src/vault/services/vault-filter.service.ts +++ b/apps/browser/src/vault/services/vault-filter.service.ts @@ -38,7 +38,7 @@ export class VaultFilterService extends BaseVaultFilterService { this.vaultFilter.myVaultOnly = false; this.vaultFilter.selectedOrganizationId = null; - this.accountService.activeAccount$.subscribe((account) => { + accountService.activeAccount$.subscribe((account) => { this.setVaultFilter(this.allVaults); }); } diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index a1fec7a747..c8a6f31424 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { EventType } from "@bitwarden/common/enums"; import { CardExport } from "@bitwarden/common/models/export/card.export"; @@ -479,10 +480,18 @@ export class GetCommand extends DownloadCommand { private async getOrganization(id: string) { let org: Organization = null; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (!userId) { + return Response.badRequest("No user found."); + } if (Utils.isGuid(id)) { - org = await this.organizationService.getFromState(id); + org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === id))), + ); } else if (id.trim() !== "") { - let orgs = await firstValueFrom(this.organizationService.organizations$); + let orgs = await firstValueFrom(this.organizationService.organizations$(userId)); orgs = CliUtils.searchOrganizations(orgs, id); if (orgs.length > 1) { return Response.multipleResults(orgs.map((c) => c.id)); diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 92da86b696..5e01af798a 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -13,6 +13,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -177,7 +178,15 @@ export class ListCommand { if (!Utils.isGuid(options.organizationId)) { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } - const organization = await this.organizationService.getFromState(options.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizatons) => organizatons.find((o) => o.id == options.organizationId))), + ); if (organization == null) { return Response.error("Organization not found."); } @@ -210,7 +219,16 @@ export class ListCommand { if (!Utils.isGuid(options.organizationId)) { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } - const organization = await this.organizationService.getFromState(options.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizatons) => organizatons.find((o) => o.id == options.organizationId))), + ); if (organization == null) { return Response.error("Organization not found."); } @@ -236,7 +254,12 @@ export class ListCommand { } private async listOrganizations(options: Options) { - let organizations = await firstValueFrom(this.organizationService.memberOrganizations$); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + let organizations = await firstValueFrom(this.organizationService.memberOrganizations$(userId)); if (options.search != null && options.search.trim() !== "") { organizations = CliUtils.searchOrganizations(organizations, options.search); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index f57db9909d..6c71564061 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; import * as path from "path"; import * as jsdom from "jsdom"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { OrganizationUserApiService, @@ -25,8 +25,8 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; @@ -36,7 +36,10 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; +import { + AccountServiceImplementation, + getUserId, +} from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; @@ -238,7 +241,8 @@ export class ServiceContainer { stateService: StateService; autofillSettingsService: AutofillSettingsServiceAbstraction; domainSettingsService: DomainSettingsService; - organizationService: OrganizationService; + organizationService: DefaultOrganizationService; + DefaultOrganizationService: DefaultOrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; folderApiService: FolderApiService; @@ -450,7 +454,7 @@ export class ServiceContainer { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.organizationService = new OrganizationService(this.stateProvider); + this.organizationService = new DefaultOrganizationService(this.stateProvider); this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( @@ -824,6 +828,7 @@ export class ServiceContainer { this.cipherAuthorizationService = new DefaultCipherAuthorizationService( this.collectionService, this.organizationService, + this.accountService, ); } @@ -831,7 +836,7 @@ export class ServiceContainer { this.authService.logOut(() => { /* Do nothing */ }); - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); await Promise.all([ this.eventUploadService.uploadEvents(userId as UserId), this.keyService.clearKeys(), diff --git a/apps/cli/src/tools/import.command.ts b/apps/cli/src/tools/import.command.ts index a71c9177d2..f826cb24b7 100644 --- a/apps/cli/src/tools/import.command.ts +++ b/apps/cli/src/tools/import.command.ts @@ -2,8 +2,10 @@ // @ts-strict-ignore import { OptionValues } from "commander"; import * as inquirer from "inquirer"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer/core"; @@ -16,12 +18,23 @@ export class ImportCommand { private importService: ImportServiceAbstraction, private organizationService: OrganizationService, private syncService: SyncService, + private accountService: AccountService, ) {} async run(format: ImportType, filepath: string, options: OptionValues): Promise { const organizationId = options.organizationid; if (organizationId != null) { - const organization = await this.organizationService.getFromState(organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (organization == null) { return Response.badRequest( diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 3eb0e68de0..f3eb6eef61 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -450,6 +450,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.importService, this.serviceContainer.organizationService, this.serviceContainer.syncService, + this.serviceContainer.accountService, ); const response = await command.run(format, filepath, options); this.processResponse(response); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index a28d070d19..73027e8011 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -202,7 +202,17 @@ export class CreateCommand { if (orgKey == null) { throw new Error("No encryption key for this organization."); } - const organization = await this.organizationService.get(req.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + if (!userId) { + return Response.badRequest("No user found."); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === req.organizationId))), + ); const currentOrgUserId = organization.organizationUserId; const groups = diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index a05b09e139..ea2798ff2d 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -32,6 +32,7 @@ import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstrac import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; @@ -872,9 +873,10 @@ export class AppComponent implements OnInit, OnDestroy { } private async deleteAccount() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning).pipe( - withLatestFrom(this.organizationService.organizations$), + withLatestFrom(this.organizationService.organizations$(userId)), map(async ([accountDeprovisioningEnabled, organization]) => { if ( accountDeprovisioningEnabled && diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts index 5d61f7cf25..f5fce0e5e4 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts @@ -5,11 +5,16 @@ import { TestBed } from "@angular/core/testing"; import { provideRouter } from "@angular/router"; import { RouterTestingHarness } from "@angular/router/testing"; import { MockProxy, any, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { isEnterpriseOrgGuard } from "./is-enterprise-org.guard"; @@ -44,15 +49,19 @@ describe("Is Enterprise Org Guard", () => { let organizationService: MockProxy; let dialogService: MockProxy; let routerHarness: RouterTestingHarness; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(async () => { organizationService = mock(); dialogService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ providers: [ { provide: OrganizationService, useValue: organizationService }, { provide: DialogService, useValue: dialogService }, + { provide: AccountService, useValue: accountService }, provideRouter([ { path: "", @@ -82,7 +91,7 @@ describe("Is Enterprise Org Guard", () => { it("redirects to `/` if the organization id provided is not found", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(null); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([])); await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This is the home screen!", @@ -101,7 +110,7 @@ describe("Is Enterprise Org Guard", () => { type: OrganizationUserType.User, productTierType: productTierType, }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); expect(dialogService.openSimpleDialog).toHaveBeenCalled(); expect( @@ -115,7 +124,7 @@ describe("Is Enterprise Org Guard", () => { type: OrganizationUserType.Owner, productTierType: ProductTierType.Teams, }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); dialogService.openSimpleDialog.calledWith(any()).mockResolvedValue(true); await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( @@ -133,7 +142,7 @@ describe("Is Enterprise Org Guard", () => { type: OrganizationUserType.User, productTierType: productTierType, }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnlyNoError`); expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); expect( @@ -143,7 +152,7 @@ describe("Is Enterprise Org Guard", () => { it("proceeds with navigation if the organization in question is a enterprise organization", async () => { const org = orgFactory({ productTierType: ProductTierType.Enterprise }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This component can only be accessed by a enterprise organization!", diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts index e1f3a1b825..c79abed583 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts @@ -7,8 +7,13 @@ import { Router, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { DialogService } from "@bitwarden/components"; @@ -23,9 +28,15 @@ export function isEnterpriseOrgGuard(showError: boolean = true): CanActivateFn { return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { const router = inject(Router); const organizationService = inject(OrganizationService); + const accountService = inject(AccountService); const dialogService = inject(DialogService); - const org = await organizationService.get(route.params.organizationId); + const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id))); + const org = await firstValueFrom( + organizationService + .organizations$(userId) + .pipe(getOrganizationById(route.params.organizationId)), + ); if (org == null) { return router.createUrlTree(["/"]); diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts index 6572b8aabd..8efed8cefa 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts @@ -5,10 +5,15 @@ import { TestBed } from "@angular/core/testing"; import { provideRouter } from "@angular/router"; import { RouterTestingHarness } from "@angular/router/testing"; import { MockProxy, any, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { DialogService } from "@bitwarden/components"; import { isPaidOrgGuard } from "./is-paid-org.guard"; @@ -43,15 +48,19 @@ describe("Is Paid Org Guard", () => { let organizationService: MockProxy; let dialogService: MockProxy; let routerHarness: RouterTestingHarness; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(async () => { organizationService = mock(); dialogService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ providers: [ { provide: OrganizationService, useValue: organizationService }, { provide: DialogService, useValue: dialogService }, + { provide: AccountService, useValue: accountService }, provideRouter([ { path: "", @@ -75,7 +84,7 @@ describe("Is Paid Org Guard", () => { it("redirects to `/` if the organization id provided is not found", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(null); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([])); await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This is the home screen!", @@ -86,7 +95,7 @@ describe("Is Paid Org Guard", () => { // `useTotp` is the current indicator of a free org, it is the baseline // feature offered above the free organization level. const org = orgFactory({ type: OrganizationUserType.User, useTotp: false }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); expect(dialogService.openSimpleDialog).toHaveBeenCalled(); expect( @@ -98,7 +107,7 @@ describe("Is Paid Org Guard", () => { // `useTotp` is the current indicator of a free org, it is the baseline // feature offered above the free organization level. const org = orgFactory({ type: OrganizationUserType.Owner, useTotp: false }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); dialogService.openSimpleDialog.calledWith(any()).mockResolvedValue(true); await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( @@ -108,7 +117,7 @@ describe("Is Paid Org Guard", () => { it("proceeds with navigation if the organization in question is a paid organization", async () => { const org = orgFactory({ useTotp: true }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This component can only be accessed by a paid organization!", diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts index 0fe95e3d3a..9a59c884ee 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts @@ -7,8 +7,13 @@ import { Router, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DialogService } from "@bitwarden/components"; /** @@ -22,9 +27,15 @@ export function isPaidOrgGuard(): CanActivateFn { return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { const router = inject(Router); const organizationService = inject(OrganizationService); + const accountService = inject(AccountService); const dialogService = inject(DialogService); - const org = await organizationService.get(route.params.organizationId); + const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id))); + const org = await firstValueFrom( + organizationService + .organizations$(userId) + .pipe(getOrganizationById(route.params.organizationId)), + ); if (org == null) { return router.createUrlTree(["/"]); diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts index 39f5dc429b..619b9cc442 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts @@ -8,11 +8,16 @@ import { RouterStateSnapshot, } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; @@ -34,10 +39,13 @@ describe("Organization Permissions Guard", () => { let organizationService: MockProxy; let state: MockProxy; let route: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { router = mock(); organizationService = mock(); + accountService = mockAccountServiceWith(userId); state = mock(); route = mock({ params: { @@ -48,6 +56,7 @@ describe("Organization Permissions Guard", () => { TestBed.configureTestingModule({ providers: [ { provide: Router, useValue: router }, + { provide: AccountService, useValue: accountService }, { provide: OrganizationService, useValue: organizationService }, { provide: ToastService, useValue: mock() }, { provide: I18nService, useValue: mock() }, @@ -57,7 +66,7 @@ describe("Organization Permissions Guard", () => { }); it("blocks navigation if organization does not exist", async () => { - organizationService.get.mockReturnValue(null); + organizationService.organizations$.mockReturnValue(of([])); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard()(route, state), @@ -68,7 +77,7 @@ describe("Organization Permissions Guard", () => { it("permits navigation if no permissions are specified", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const actual = await TestBed.runInInjectionContext(async () => organizationPermissionsGuard()(route, state), @@ -81,7 +90,7 @@ describe("Organization Permissions Guard", () => { const permissionsCallback = jest.fn(); permissionsCallback.mockImplementation((_org) => true); const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard(permissionsCallback)(route, state), @@ -103,7 +112,7 @@ describe("Organization Permissions Guard", () => { }); const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard(permissionsCallback)(route, state), @@ -122,7 +131,7 @@ describe("Organization Permissions Guard", () => { }), }); const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard((_org: Organization) => false)(route, state), @@ -141,7 +150,7 @@ describe("Organization Permissions Guard", () => { type: OrganizationUserType.Admin, enabled: false, }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard()(route, state), @@ -155,7 +164,7 @@ describe("Organization Permissions Guard", () => { type: OrganizationUserType.Owner, enabled: false, }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard()(route, state), diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts index 6bb881762c..ea9bfad389 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts @@ -7,12 +7,14 @@ import { Router, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { canAccessOrgAdmin, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; @@ -46,13 +48,21 @@ export function organizationPermissionsGuard( const toastService = inject(ToastService); const i18nService = inject(I18nService); const syncService = inject(SyncService); + const accountService = inject(AccountService); // TODO: We need to fix issue once and for all. if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const org = await organizationService.get(route.params.organizationId); + const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id))); + + const org = await firstValueFrom( + organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((org) => route.params.organizationId))), + ); + if (org == null) { return router.createUrlTree(["/"]); } diff --git a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts index 264f2c6d4a..fa348867a8 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts @@ -5,10 +5,15 @@ import { TestBed } from "@angular/core/testing"; import { provideRouter } from "@angular/router"; import { RouterTestingHarness } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { organizationRedirectGuard } from "./org-redirect.guard"; @@ -41,13 +46,17 @@ const orgFactory = (props: Partial = {}) => describe("Organization Redirect Guard", () => { let organizationService: MockProxy; let routerHarness: RouterTestingHarness; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(async () => { organizationService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ providers: [ { provide: OrganizationService, useValue: organizationService }, + { provide: AccountService, useValue: accountService }, provideRouter([ { path: "", @@ -89,16 +98,16 @@ describe("Organization Redirect Guard", () => { it("redirects to `/` if the organization id provided is not found", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(null); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([])); await routerHarness.navigateByUrl(`organizations/${org.id}/noCallback`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This is the home screen!", ); }); - it("redirects to `/organizations/{id}` if no custom redirect is supplied but the user can access the admin onsole", async () => { + it("redirects to `/organizations/{id}` if no custom redirect is supplied but the user can access the admin console", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/noCallback`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This is the admin console!", @@ -107,7 +116,7 @@ describe("Organization Redirect Guard", () => { it("redirects properly when the redirect callback returns a single string", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/stringCallback`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This is a subroute of the admin console!", @@ -116,7 +125,7 @@ describe("Organization Redirect Guard", () => { it("redirects properly when the redirect callback returns an array of strings", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); await routerHarness.navigateByUrl(`organizations/${org.id}/arrayCallback`); expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( "This is a subroute of the admin console!", diff --git a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts index 1ab73195e4..d0352586ef 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts @@ -5,12 +5,14 @@ import { Router, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { canAccessOrgAdmin, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; /** * @@ -25,8 +27,25 @@ export function organizationRedirectGuard( return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const router = inject(Router); const organizationService = inject(OrganizationService); + const accountService = inject(AccountService); - const org = await organizationService.get(route.params.organizationId); + const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id))); + + if (!userId) { + return router.createUrlTree(["/"]); + } + + const org = await firstValueFrom( + organizationService + .organizations$(userId) + .pipe( + map((organizations) => organizations.find((o) => o.id === route.params.organizationId)), + ), + ); + + if (!org) { + return router.createUrlTree(["/"]); + } if (customRedirect != null) { let redirectPath = customRedirect(org); diff --git a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts index e3edb41de7..80c12af852 100644 --- a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts +++ b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts @@ -4,8 +4,12 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { Observable, switchMap } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { IntegrationType } from "@bitwarden/common/enums"; import { HeaderModule } from "../../../layouts/header/header.module"; @@ -34,13 +38,22 @@ export class AdminConsoleIntegrationsComponent implements OnInit { ngOnInit(): void { this.organization$ = this.route.params.pipe( - switchMap((params) => this.organizationService.get$(params.organizationId)), + switchMap((params) => + this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), ); } constructor( private route: ActivatedRoute, private organizationService: OrganizationService, + private accountService: AccountService, ) { this.integrationsList = [ { diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index c1112c51e3..f65da9b87a 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, RouterModule } from "@angular/router"; -import { combineLatest, filter, map, Observable, switchMap } from "rxjs"; +import { combineLatest, filter, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -20,6 +20,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -66,14 +68,16 @@ export class OrganizationLayoutComponent implements OnInit { private configService: ConfigService, private policyService: PolicyService, private providerService: ProviderService, + private accountService: AccountService, ) {} async ngOnInit() { document.body.classList.remove("layout_frontend"); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organization$ = this.route.params.pipe( map((p) => p.organizationId), - switchMap((id) => this.organizationService.organizations$.pipe(getById(id))), + switchMap((id) => this.organizationService.organizations$(userId).pipe(getById(id))), filter((org) => org != null), ); diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index ab7306d914..c6969f5b55 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -2,14 +2,19 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { concatMap, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventSystemUser } from "@bitwarden/common/enums"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; @@ -55,6 +60,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe private providerService: ProviderService, fileDownloadService: FileDownloadService, toastService: ToastService, + private accountService: AccountService, ) { super( eventService, @@ -68,11 +74,16 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe } async ngOnInit() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.route.params .pipe( concatMap(async (params) => { this.organizationId = params.organizationId; - this.organization = await this.organizationService.get(this.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); if (this.organization == null || !this.organization.useEvents) { await this.router.navigate(["/organizations", this.organizationId]); return; diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index bbb9af67e3..330ffe86f0 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -13,6 +13,7 @@ import { of, shareReplay, Subject, + switchMap, takeUntil, } from "rxjs"; @@ -22,7 +23,10 @@ import { OrganizationUserApiService, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -97,9 +101,14 @@ export const openGroupAddEditDialog = ( templateUrl: "group-add-edit.component.html", }) export class GroupAddEditComponent implements OnInit, OnDestroy { - private organization$ = this.organizationService - .get$(this.organizationId) - .pipe(shareReplay({ refCount: true })); + private organization$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe(getOrganizationById(this.organizationId)) + .pipe(shareReplay({ refCount: true })), + ), + ); protected PermissionMode = PermissionMode; protected ResultType = GroupAddEditDialogResultType; diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 4f43dacad3..fbf29602e0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -22,7 +22,10 @@ import { OrganizationUserApiService, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType, OrganizationUserType, @@ -156,9 +159,14 @@ export class MemberDialogComponent implements OnDestroy { private toastService: ToastService, private configService: ConfigService, ) { - this.organization$ = organizationService - .get$(this.params.organizationId) - .pipe(shareReplay({ refCount: true, bufferSize: 1 })); + this.organization$ = accountService.activeAccount$.pipe( + switchMap((account) => + organizationService + .organizations$(account?.id) + .pipe(getOrganizationById(this.params.organizationId)) + .pipe(shareReplay({ refCount: true, bufferSize: 1 })), + ), + ); this.editMode = this.params.organizationUserId != null; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 703f187b22..57b352a204 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -28,7 +28,10 @@ import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { PolicyApiServiceAbstraction as PolicyApiService } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -40,6 +43,7 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -122,6 +126,7 @@ export class MembersComponent extends BaseMembersComponent private route: ActivatedRoute, private syncService: SyncService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserApiService: OrganizationUserApiService, private router: Router, @@ -144,7 +149,15 @@ export class MembersComponent extends BaseMembersComponent ); const organization$ = this.route.params.pipe( - concatMap((params) => this.organizationService.get$(params.organizationId)), + concatMap((params) => + this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), shareReplay({ refCount: true, bufferSize: 1 }), ); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts index da71b61892..9b595859b2 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationUserApiService, @@ -164,10 +165,9 @@ describe("OrganizationUserResetPasswordService", () => { describe("getRotatedData", () => { beforeEach(() => { - organizationService.getAll.mockResolvedValue([ - createOrganization("1", "org1"), - createOrganization("2", "org2"), - ]); + organizationService.organizations$.mockReturnValue( + of([createOrganization("1", "org1"), createOrganization("2", "org2")]), + ); organizationApiService.getKeys.mockResolvedValue( new OrganizationKeysResponse({ privateKey: "test-private-key", diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index 77d5ad29cc..0fe9b75aa9 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { OrganizationUserApiService, @@ -154,7 +155,7 @@ export class OrganizationUserResetPasswordService throw new Error("New user key is required for rotation."); } - const allOrgs = await this.organizationService.getAll(); + const allOrgs = await firstValueFrom(this.organizationService.organizations$(userId)); if (!allOrgs) { return; diff --git a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts index a000106068..328989df66 100644 --- a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts @@ -2,11 +2,17 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +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 { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -43,6 +49,7 @@ export class MasterPasswordPolicyComponent extends BasePolicyComponent implement private formBuilder: FormBuilder, i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, ) { super(); @@ -58,7 +65,12 @@ export class MasterPasswordPolicyComponent extends BasePolicyComponent implement async ngOnInit() { super.ngOnInit(); - const organization = await this.organizationService.get(this.policyResponse.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.policyResponse.organizationId)), + ); this.showKeyConnectorInfo = organization.keyConnectorEnabled; } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 9e96215a6c..3354c7c5e1 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -2,14 +2,18 @@ // @ts-strict-ignore import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { lastValueFrom } from "rxjs"; -import { first } from "rxjs/operators"; +import { firstValueFrom, lastValueFrom } from "rxjs"; +import { first, map } from "rxjs/operators"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DialogService } from "@bitwarden/components"; import { PolicyListService } from "../../core/policy-list.service"; @@ -37,6 +41,7 @@ export class PoliciesComponent implements OnInit { constructor( private route: ActivatedRoute, private organizationService: OrganizationService, + private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, private policyListService: PolicyListService, private dialogService: DialogService, @@ -46,7 +51,14 @@ export class PoliciesComponent implements OnInit { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organizationId = params.organizationId; - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.policies = this.policyListService.getPolicies(); await this.load(); diff --git a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts index 6307ee13fa..80e4e5254f 100644 --- a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts @@ -1,9 +1,15 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -31,13 +37,29 @@ export class ResetPasswordPolicyComponent extends BasePolicyComponent implements constructor( private formBuilder: FormBuilder, private organizationService: OrganizationService, + private accountService: AccountService, ) { super(); } async ngOnInit() { super.ngOnInit(); - const organization = await this.organizationService.get(this.policyResponse.organizationId); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + throw new Error("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.policyResponse.organizationId)), + ); + + if (!organization) { + throw new Error("No organization found."); + } this.showKeyConnectorInfo = organization.keyConnectorEnabled; } } diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 2b2e1f7049..6090396293 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -2,9 +2,14 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; -import { filter, map, Observable, startWith, concatMap } from "rxjs"; +import { filter, map, Observable, startWith, concatMap, firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { ReportVariant, reports, ReportType, ReportEntry } from "../../../tools/reports"; @@ -20,6 +25,7 @@ export class ReportsHomeComponent implements OnInit { constructor( private route: ActivatedRoute, private organizationService: OrganizationService, + private accountService: AccountService, private router: Router, ) {} @@ -30,8 +36,14 @@ export class ReportsHomeComponent implements OnInit { startWith(this.isReportsHomepageRouteUrl(this.router.url)), ); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.reports$ = this.route.params.pipe( - concatMap((params) => this.organizationService.get$(params.organizationId)), + concatMap((params) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), map((org) => this.buildReports(org.productTierType)), ); } diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 0cf8c24d46..0cc0f85a00 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -3,15 +3,29 @@ import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, from, lastValueFrom, of, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatest, + firstValueFrom, + from, + lastValueFrom, + of, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationCollectionManagementUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-collection-management-update.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request"; import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -77,6 +91,7 @@ export class AccountComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private keyService: KeyService, private router: Router, + private accountService: AccountService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, @@ -88,9 +103,14 @@ export class AccountComponent implements OnInit, OnDestroy { async ngOnInit() { this.selfHosted = this.platformUtilsService.isSelfHost(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.route.params .pipe( - switchMap((params) => this.organizationService.get$(params.organizationId)), + switchMap((params) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), switchMap((organization) => { return combineLatest([ of(organization), diff --git a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts index 680fb014b4..2f801ac777 100644 --- a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts @@ -3,12 +3,17 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; -import { combineLatest, Subject, takeUntil } from "rxjs"; +import { combineLatest, firstValueFrom, Subject, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Verification } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -94,6 +99,7 @@ export class DeleteOrganizationDialogComponent implements OnInit, OnDestroy { private userVerificationService: UserVerificationService, private cipherService: CipherService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, private toastService: ToastService, @@ -106,9 +112,12 @@ export class DeleteOrganizationDialogComponent implements OnInit, OnDestroy { async ngOnInit(): Promise { this.deleteOrganizationRequestType = this.params.requestType; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); combineLatest([ - this.organizationService.get$(this.params.organizationId), + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.params.organizationId)), this.cipherService.getAllFromApiForOrganization(this.params.organizationId), ]) .pipe(takeUntil(this.destroy$)) diff --git a/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts b/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts index 49ec7465da..5ea7e2ab52 100644 --- a/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts @@ -2,13 +2,15 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { canAccessVaultTab, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ImportCollectionServiceAbstraction } from "@bitwarden/importer/core"; import { ImportComponent } from "@bitwarden/importer/ui"; @@ -36,6 +38,7 @@ export class OrgImportComponent implements OnInit { private route: ActivatedRoute, private organizationService: OrganizationService, private router: Router, + private accountService: AccountService, ) {} ngOnInit(): void { @@ -46,7 +49,12 @@ export class OrgImportComponent implements OnInit { * Callback that is called after a successful import. */ protected async onSuccessfulImport(organizationId: string): Promise { - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (organization == null) { return; } diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index 9cc6341c08..bc28270d3a 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -3,16 +3,20 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, takeUntil, map, lastValueFrom } from "rxjs"; +import { concatMap, takeUntil, map, lastValueFrom, firstValueFrom } from "rxjs"; import { first, tap } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/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 { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -38,7 +42,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme private route: ActivatedRoute, private organizationService: OrganizationService, billingAccountProfileStateService: BillingAccountProfileStateService, - accountService: AccountService, + protected accountService: AccountService, ) { super( dialogService, @@ -52,11 +56,13 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme } async ngOnInit() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.route.params .pipe( concatMap((params) => this.organizationService - .get$(params.organizationId) + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)) .pipe(map((organization) => ({ params, organization }))), ), tap(async (mapResponse) => { diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts index d2d2f98d0a..fdad689d98 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -3,7 +3,7 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { lastValueFrom, Observable, Subject } from "rxjs"; +import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs"; import { first, map, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -12,6 +12,8 @@ import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationSponsorshipRedeemRequest } from "@bitwarden/common/admin-console/models/request/organization/organization-sponsorship-redeem.request"; import { PreValidateSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/pre-validate-sponsorship.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -69,6 +71,7 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { private syncService: SyncService, private validationService: ValidationService, private organizationService: OrganizationService, + private accountService: AccountService, private dialogService: DialogService, private formBuilder: FormBuilder, private toastService: ToastService, @@ -115,14 +118,18 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { this.loading = false; }); - this.existingFamilyOrganizations$ = this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter( - (o) => - o.productTierType === ProductTierType.Families && o.type === OrganizationUserType.Owner, + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.existingFamilyOrganizations$ = this.organizationService + .organizations$(userId) + .pipe( + map((orgs) => + orgs.filter( + (o) => + o.productTierType === ProductTierType.Families && + o.type === OrganizationUserType.Owner, + ), ), - ), - ); + ); this.existingFamilyOrganizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { if (orgs.length === 0) { diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index baa49b8b13..2a972198cc 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -2,10 +2,15 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -32,6 +37,7 @@ export class ExposedPasswordsReportComponent auditService: AuditService, modalService: ModalService, organizationService: OrganizationService, + protected accountService: AccountService, private route: ActivatedRoute, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -41,6 +47,7 @@ export class ExposedPasswordsReportComponent cipherService, auditService, organizationService, + accountService, modalService, passwordRepromptService, i18nService, @@ -52,7 +59,14 @@ export class ExposedPasswordsReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - this.organization = await this.organizationService.get(params.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); this.manageableCiphers = await this.cipherService.getAll(); }); } diff --git a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts index 461e7691fa..4ddbd58efc 100644 --- a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts @@ -2,9 +2,14 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -31,12 +36,14 @@ export class InactiveTwoFactorReportComponent logService: LogService, passwordRepromptService: PasswordRepromptService, organizationService: OrganizationService, + accountService: AccountService, i18nService: I18nService, syncService: SyncService, ) { super( cipherService, organizationService, + accountService, modalService, logService, passwordRepromptService, @@ -49,7 +56,14 @@ export class InactiveTwoFactorReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - this.organization = await this.organizationService.get(params.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); await super.ngOnInit(); }); } diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index 176fad24d2..3609a9fe14 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -2,9 +2,14 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -31,6 +36,7 @@ export class ReusedPasswordsReportComponent modalService: ModalService, private route: ActivatedRoute, organizationService: OrganizationService, + protected accountService: AccountService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, syncService: SyncService, @@ -38,6 +44,7 @@ export class ReusedPasswordsReportComponent super( cipherService, organizationService, + accountService, modalService, passwordRepromptService, i18nService, @@ -49,7 +56,14 @@ export class ReusedPasswordsReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - this.organization = await this.organizationService.get(params.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); this.manageableCiphers = await this.cipherService.getAll(); await super.ngOnInit(); }); diff --git a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts index 631890a976..f6c1f9cff6 100644 --- a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts @@ -2,10 +2,15 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -29,6 +34,7 @@ export class UnsecuredWebsitesReportComponent modalService: ModalService, private route: ActivatedRoute, organizationService: OrganizationService, + protected accountService: AccountService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, syncService: SyncService, @@ -37,6 +43,7 @@ export class UnsecuredWebsitesReportComponent super( cipherService, organizationService, + accountService, modalService, passwordRepromptService, i18nService, @@ -49,7 +56,14 @@ export class UnsecuredWebsitesReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - this.organization = await this.organizationService.get(params.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); await super.ngOnInit(); }); } diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index d65682d662..2bd6285d4f 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -2,9 +2,14 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -36,11 +41,13 @@ export class WeakPasswordsReportComponent passwordRepromptService: PasswordRepromptService, i18nService: I18nService, syncService: SyncService, + protected accountService: AccountService, ) { super( cipherService, passwordStrengthService, organizationService, + accountService, modalService, passwordRepromptService, i18nService, @@ -52,7 +59,14 @@ export class WeakPasswordsReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - this.organization = await this.organizationService.get(params.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); this.manageableCiphers = await this.cipherService.getAll(); await super.ngOnInit(); }); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index ee9f87bc2c..825b610bab 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -17,6 +17,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -219,7 +220,10 @@ export class AppComponent implements OnDestroy, OnInit { break; case "syncOrganizationStatusChanged": { const { organizationId, enabled } = message; - const organizations = await firstValueFrom(this.organizationService.organizations$); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organizations = await firstValueFrom( + this.organizationService.organizations$(userId), + ); const organization = organizations.find((org) => org.id === organizationId); if (organization) { @@ -227,21 +231,27 @@ export class AppComponent implements OnDestroy, OnInit { ...organization, enabled: enabled, }; - await this.organizationService.upsert(updatedOrganization); + await this.organizationService.upsert(updatedOrganization, userId); } break; } case "syncOrganizationCollectionSettingChanged": { const { organizationId, limitCollectionCreation, limitCollectionDeletion } = message; - const organizations = await firstValueFrom(this.organizationService.organizations$); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organizations = await firstValueFrom( + this.organizationService.organizations$(userId), + ); const organization = organizations.find((org) => org.id === organizationId); if (organization) { - await this.organizationService.upsert({ - ...organization, - limitCollectionCreation: limitCollectionCreation, - limitCollectionDeletion: limitCollectionDeletion, - }); + await this.organizationService.upsert( + { + ...organization, + limitCollectionCreation: limitCollectionCreation, + limitCollectionDeletion: limitCollectionDeletion, + }, + userId, + ); } break; } @@ -291,7 +301,7 @@ export class AppComponent implements OnDestroy, OnInit { // will prevent any toasts from being displayed long enough to be read await this.eventUploadService.uploadEvents(); - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const logoutPromise = firstValueFrom( this.authService.authStatusFor$(userId).pipe( diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index 7e1be937a2..012fa0ff01 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { combineLatest, from, lastValueFrom, map, Observable } from "rxjs"; +import { combineLatest, firstValueFrom, from, lastValueFrom, map, Observable } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; @@ -33,16 +35,21 @@ export class AccountComponent implements OnInit { private userVerificationService: UserVerificationService, private configService: ConfigService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async ngOnInit() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.AccountDeprovisioning, ); - const userIsManagedByOrganization$ = this.organizationService.organizations$.pipe( - map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), - ); + const userIsManagedByOrganization$ = this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), + ); const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword()); diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index 4f4920270f..7273136380 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -49,16 +50,21 @@ export class ProfileComponent implements OnInit, OnDestroy { this.fingerprintMaterial = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.managingOrganization$ = this.configService .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) .pipe( switchMap((isAccountDeprovisioningEnabled) => isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map((organizations) => - organizations.find((o) => o.userIsManagedByOrganization === true), - ), - ) + ? this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) : of(null), ), ); diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 8f0f195440..ebbc762e5b 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -12,6 +12,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -188,7 +189,7 @@ export class ChangePasswordComponent await this.kdfConfigService.getKdfConfig(), ); - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const newLocalKeyHash = await this.keyService.hashMasterKey( this.masterPassword, newMasterKey, diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index 73e32add5c..83bdfffbe4 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -8,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -83,7 +84,8 @@ export class EmergencyAccessComponent implements OnInit { } async ngOnInit() { - const orgs = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); this.isOrganizationOwner = orgs.some((o) => o.isOwner); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index 0ad7eef81b..c5114c0be6 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -43,6 +43,7 @@ describe("EmergencyViewDialogComponent", () => { imports: [EmergencyViewDialogComponent, NoopAnimationsModule], providers: [ { provide: OrganizationService, useValue: mock() }, + { provide: AccountService, useValue: accountService }, { provide: CollectionService, useValue: mock() }, { provide: FolderService, useValue: mock() }, { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 3b20718873..c561d92d75 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -71,7 +71,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { protected messagingService: MessagingService, protected policyService: PolicyService, billingAccountProfileStateService: BillingAccountProfileStateService, - private accountService: AccountService, + protected accountService: AccountService, ) { this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => diff --git a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts index 0f6baa5f32..75054d78db 100644 --- a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts +++ b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts @@ -1,15 +1,35 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; export const organizationIsUnmanaged: CanActivateFn = async (route: ActivatedRouteSnapshot) => { const organizationService = inject(OrganizationService); const providerService = inject(ProviderService); + const accountService = inject(AccountService); - const organization = await organizationService.get(route.params.organizationId); + const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id))); + + if (!userId) { + throw new Error("No user found."); + } + + const organization = await firstValueFrom( + organizationService + .organizations$(userId) + .pipe(getOrganizationById(route.params.organizationId)), + ); + + if (!organization) { + throw new Error("No organization found."); + } if (!organization.hasProvider) { return true; diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 0166560007..786c25c8d4 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; @@ -37,6 +42,7 @@ export class AdjustSubscription implements OnInit, OnDestroy { private formBuilder: FormBuilder, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -73,14 +79,19 @@ export class AdjustSubscription implements OnInit, OnDestroy { request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 14b6a132ee..54f1860c91 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -13,17 +13,21 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, map, takeUntil } from "rxjs"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction, BillingInformation, @@ -209,6 +213,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, private organizationBillingService: OrganizationBillingService, ) {} @@ -226,7 +231,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.organizationId = this.dialogParams.organizationId; this.currentPlan = this.sub?.plan; this.selectedPlan = this.sub?.plan; - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); if (this.deprecateStripeSourcesAPI) { const { accountCredit, paymentSource } = await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 94e8637e8a..0b09ac2c6d 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -11,13 +11,16 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; -import { debounceTime } from "rxjs/operators"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { debounceTime, map } from "rxjs/operators"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -27,6 +30,7 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-organization-create.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; @@ -179,6 +183,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } @@ -189,7 +194,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { ); if (this.organizationId) { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.billing = await this.organizationApiService.getBilling(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId); diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 0805e92ee2..7f81a1fe23 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -6,9 +6,14 @@ import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -76,6 +81,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private i18nService: I18nService, private logService: LogService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, private dialogService: DialogService, @@ -117,7 +123,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy async load() { this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const isIndependentOrganizationOwner = !this.userOrg.hasProvider && this.userOrg.isOwner; const isResoldOrganizationOwner = this.userOrg.hasReseller && this.userOrg.isOwner; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index ef68de3952..e6854a5216 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -7,10 +7,15 @@ import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationConnectionType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { BillingSyncConfigApi } from "@bitwarden/common/billing/models/api/billing-sync-config.api"; import { SelfHostedOrganizationSubscriptionView } from "@bitwarden/common/billing/models/view/self-hosted-organization-subscription.view"; @@ -80,6 +85,7 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest private messagingService: MessagingService, private apiService: ApiService, private organizationService: OrganizationService, + private accountService: AccountService, private route: ActivatedRoute, private organizationApiService: OrganizationApiServiceAbstraction, private platformUtilsService: PlatformUtilsService, @@ -115,7 +121,12 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest return; } this.loading = true; - this.userOrg = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.userOrg = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); this.showAutomaticSyncAndManualUpload = this.userOrg.productTierType == ProductTierType.Families ? false : true; if (this.userOrg.canViewSubscription) { diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 270ba54f70..a5b18d9edb 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -4,11 +4,15 @@ import { Location } from "@angular/common"; import { Component, OnDestroy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; -import { from, lastValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, from, lastValueFrom, map, switchMap } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; @@ -60,6 +64,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { private location: Location, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { this.activatedRoute.params @@ -120,7 +125,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.organizationSubscriptionResponse, this.organization] = await Promise.all([ organizationSubscriptionPromise, diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index fc7a188f96..c10c9abc9b 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -2,11 +2,16 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + InternalOrganizationServiceAbstraction, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -107,6 +112,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest private platformUtilsService: PlatformUtilsService, private toastService: ToastService, private internalOrganizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -165,14 +171,19 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest request, ); - const organization = await this.internalOrganizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const organization = await firstValueFrom( + this.internalOrganizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const organizationData = new OrganizationData(response, { isMember: organization.isMember, isProviderUser: organization.isProviderUser, }); - await this.internalOrganizationService.upsert(organizationData); + await this.internalOrganizationService.upsert(organizationData, userId); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 7ad0895809..617b68abb3 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -2,12 +2,15 @@ // @ts-strict-ignore import { Component, EventEmitter, Input, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request"; import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; @@ -37,6 +40,7 @@ export class SecretsManagerSubscribeStandaloneComponent { private organizationApiService: OrganizationApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, private toastService: ToastService, + private accountService: AccountService, ) {} submit = async () => { @@ -56,7 +60,8 @@ export class SecretsManagerSubscribeStandaloneComponent { isMember: this.organization.isMember, isProviderUser: this.organization.isProviderUser, }); - await this.organizationService.upsert(organizationData); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + await this.organizationService.upsert(organizationData, userId); /* Because subscribing to Secrets Manager automatically provides access to Secrets Manager for the diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index cc53e0a32b..c148bd007b 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -5,6 +5,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -25,15 +26,34 @@ export class FreeFamiliesPolicyService { constructor( private policyService: PolicyService, private organizationService: OrganizationService, + private accountService: AccountService, private configService: ConfigService, ) {} + canManageSponsorships$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.canManageSponsorships$(account?.id); + } else { + return of(); + } + }), + ); + + organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account?.id) { + return this.organizationService.organizations$(account?.id); + } else { + return of(); + } + }), + ); + get showFreeFamilies$(): Observable { return this.isFreeFamilyFlagEnabled$.pipe( switchMap((isFreeFamilyFlagEnabled) => - isFreeFamilyFlagEnabled - ? this.getFreeFamiliesVisibility$() - : this.organizationService.canManageSponsorships$, + isFreeFamilyFlagEnabled ? this.getFreeFamiliesVisibility$() : this.canManageSponsorships$, ), ); } @@ -41,7 +61,7 @@ export class FreeFamiliesPolicyService { private getFreeFamiliesVisibility$(): Observable { return combineLatest([ this.checkEnterpriseOrganizationsAndFetchPolicy(), - this.organizationService.canManageSponsorships$, + this.canManageSponsorships$, ]).pipe( map(([orgStatus, canManageSponsorships]) => this.shouldShowFreeFamilyLink(orgStatus, canManageSponsorships), @@ -61,7 +81,7 @@ export class FreeFamiliesPolicyService { } checkEnterpriseOrganizationsAndFetchPolicy(): Observable { - return this.organizationService.organizations$.pipe( + return this.organizations$.pipe( filter((organizations) => Array.isArray(organizations) && organizations.length > 0), switchMap((organizations) => this.fetchEnterpriseOrganizationPolicy(organizations)), ); diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index 5e26e80a30..b9f70dd308 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -19,6 +19,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanSponsorshipType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -90,11 +91,13 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { FeatureFlag.DisableFreeFamiliesSponsorship, ); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (this.isFreeFamilyFlagEnabled) { await this.preventAccessToFreeFamiliesPage(); this.availableSponsorshipOrgs$ = combineLatest([ - this.organizationService.organizations$, + this.organizationService.organizations$(userId), this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy), ]).pipe( map(([organizations, policies]) => @@ -111,9 +114,9 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { ), ); } else { - this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)), - ); + this.availableSponsorshipOrgs$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable))); } this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { @@ -126,9 +129,9 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); - this.activeSponsorshipOrgs$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null)), - ); + this.activeSponsorshipOrgs$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null))); this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index 71afde81ee..7860d45668 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -6,7 +6,10 @@ import { FormControl, FormGroup, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; @@ -77,7 +80,14 @@ export class AddCreditDialogComponent implements OnInit { this.creditAmount = "20.00"; } this.ppButtonCustomField = "organization_id:" + this.organizationId; - const org = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const org = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); if (org != null) { this.subject = org.name; this.name = org.name; diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 149b4adf52..4a53e503e4 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -4,12 +4,16 @@ import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { lastValueFrom } from "rxjs"; +import { firstValueFrom, lastValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -73,6 +77,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { private toastService: ToastService, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, + private accountService: AccountService, protected syncService: SyncService, ) { const state = this.router.getCurrentNavigation()?.extras?.state; @@ -117,7 +122,14 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); - const organizationPromise = this.organizationService.get(this.organizationId); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const organizationPromise = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts index b09b32d060..d64e1b817c 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts @@ -3,11 +3,12 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, map, Observable } from "rxjs"; +import { combineLatest, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import type { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { DialogService, NavigationModule } from "@bitwarden/components"; @@ -20,12 +21,17 @@ import { TrialFlowService } from "./../../billing/services/trial-flow.service"; imports: [CommonModule, JslibModule, NavigationModule], }) export class OrgSwitcherComponent { - protected organizations$: Observable = - this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), - ), - ); + protected organizations$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe( + map((orgs) => + orgs.filter((org) => this.filter(org)).sort((a, b) => a.name.localeCompare(b.name)), + ), + ), + ), + ); protected activeOrganization$: Observable = combineLatest([ this.route.paramMap, @@ -61,6 +67,7 @@ export class OrgSwitcherComponent { private organizationService: OrganizationService, private trialFlowService: TrialFlowService, protected billingApiService: BillingApiServiceAbstraction, + private accountService: AccountService, ) {} protected toggle(event?: MouseEvent) { diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index 1c15f7cc7c..eb486efe45 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -1,15 +1,17 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; @@ -22,11 +24,14 @@ import { NavigationProductSwitcherComponent } from "./navigation-switcher.compon }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +57,15 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + @Component({ selector: "story-layout", template: ``, @@ -86,6 +100,7 @@ export default { imports: [NavigationModule, RouterModule, LayoutComponent], providers: [ { provide: OrganizationService, useClass: MockOrganizationService }, + { provide: AccountService, useClass: MockAccountService }, { provide: ProviderService, useClass: MockProviderService }, { provide: SyncService, useClass: MockSyncService }, ProductSwitcherService, diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index 7a4df4bad0..e8cefa4414 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -1,15 +1,17 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components"; import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; @@ -22,11 +24,14 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; }) class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); - organizations$ = MockOrganizationService._orgs; // eslint-disable-line rxjs/no-exposed-subjects + + organizations$(): Observable { + return MockOrganizationService._orgs.asObservable(); + } @Input() set mockOrgs(orgs: Organization[]) { - this.organizations$.next(orgs); + MockOrganizationService._orgs.next(orgs); } } @@ -52,6 +57,15 @@ class MockSyncService implements Partial { } } +class MockAccountService implements Partial { + activeAccount$?: Observable = of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }); +} + @Component({ selector: "story-layout", template: ``, @@ -78,6 +92,8 @@ export default { ], imports: [JslibModule, MenuModule, IconButtonModule, LinkModule, RouterModule], providers: [ + { provide: AccountService, useClass: MockAccountService }, + MockAccountService, { provide: OrganizationService, useClass: MockOrganizationService }, MockOrganizationService, { provide: ProviderService, useClass: MockProviderService }, @@ -134,7 +150,9 @@ export default { ], } as Meta; -type Story = StoryObj; +type Story = StoryObj< + ProductSwitcherComponent & MockProviderService & MockOrganizationService & MockAccountService +>; const Template: Story = { render: (args) => ({ diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts index 919b3be042..4187900060 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts @@ -10,7 +10,11 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ProductSwitcherService } from "./product-switcher.service"; @@ -19,8 +23,10 @@ describe("ProductSwitcherService", () => { let router: { url: string; events: Observable }; let organizationService: MockProxy; let providerService: MockProxy; + let accountService: FakeAccountService; let activeRouteParams = convertToParamMap({ organizationId: "1234" }); const getLastSync = jest.fn().mockResolvedValue(new Date("2024-05-14")); + const userId = Utils.newGuid() as UserId; // The service is dependent on the SyncService, which is behind a `setTimeout` // Most of the tests don't need to test this aspect so `advanceTimersByTime` @@ -36,10 +42,11 @@ describe("ProductSwitcherService", () => { router = mock(); organizationService = mock(); providerService = mock(); + accountService = mockAccountServiceWith(userId); router.url = "/"; router.events = of({}); - organizationService.organizations$ = of([{}] as Organization[]); + organizationService.organizations$.mockReturnValue(of([{}] as Organization[])); providerService.getAll.mockResolvedValue([] as Provider[]); TestBed.configureTestingModule({ @@ -47,6 +54,7 @@ describe("ProductSwitcherService", () => { { provide: Router, useValue: router }, { provide: OrganizationService, useValue: organizationService }, { provide: ProviderService, useValue: providerService }, + { provide: AccountService, useValue: accountService }, { provide: ActivatedRoute, useValue: { @@ -111,13 +119,15 @@ describe("ProductSwitcherService", () => { }); it("is included in bento when there is an organization with SM", async () => { - organizationService.organizations$ = of([ - { - id: "1234", - canAccessSecretsManager: true, - enabled: true, - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "1234", + canAccessSecretsManager: true, + enabled: true, + }, + ] as Organization[]), + ); initiateService(); @@ -138,7 +148,9 @@ describe("ProductSwitcherService", () => { }); it("includes Admin Console in bento when a user has access to it", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); initiateService(); @@ -194,7 +206,9 @@ describe("ProductSwitcherService", () => { }); it("marks Admin Console as active", async () => { - organizationService.organizations$ = of([{ id: "1234", isOwner: true }] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([{ id: "1234", isOwner: true }] as Organization[]), + ); activeRouteParams = convertToParamMap({ organizationId: "1" }); router.url = "/organizations/"; @@ -225,20 +239,22 @@ describe("ProductSwitcherService", () => { it("updates secrets manager path when the org id is found in the path", async () => { router.url = "/sm/4243"; - organizationService.organizations$ = of([ - { - id: "23443234", - canAccessSecretsManager: true, - enabled: true, - name: "Org 2", - }, - { - id: "4243", - canAccessSecretsManager: true, - enabled: true, - name: "Org 32", - }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { + id: "23443234", + canAccessSecretsManager: true, + enabled: true, + name: "Org 2", + }, + { + id: "4243", + canAccessSecretsManager: true, + enabled: true, + name: "Org 32", + }, + ] as Organization[]), + ); initiateService(); @@ -253,10 +269,12 @@ describe("ProductSwitcherService", () => { it("updates admin console path when the org id is found in the path", async () => { router.url = "/organizations/111-22-33"; - organizationService.organizations$ = of([ - { id: "111-22-33", isOwner: true, name: "Test Org" }, - { id: "4243", isOwner: true, name: "My Org" }, - ] as Organization[]); + organizationService.organizations$.mockReturnValue( + of([ + { id: "111-22-33", isOwner: true, name: "Test Org" }, + { id: "4243", isOwner: true, name: "My Org" }, + ] as Organization[]), + ); initiateService(); diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts index 2c16886a2d..f962879d61 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts @@ -2,7 +2,16 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { ActivatedRoute, NavigationEnd, NavigationStart, ParamMap, Router } from "@angular/router"; -import { combineLatest, concatMap, filter, map, Observable, ReplaySubject, startWith } from "rxjs"; +import { + combineLatest, + concatMap, + filter, + map, + Observable, + ReplaySubject, + startWith, + switchMap, +} from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { @@ -11,6 +20,7 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SyncService } from "@bitwarden/common/platform/sync"; export type ProductSwitcherItem = { @@ -90,18 +100,20 @@ export class ProductSwitcherService { private router: Router, private i18n: I18nPipe, private syncService: SyncService, + private accountService: AccountService, ) { this.pollUntilSynced(); } + organizations$ = this.accountService.activeAccount$.pipe( + map((a) => a?.id), + switchMap((id) => this.organizationService.organizations$(id)), + ); + products$: Observable<{ bento: ProductSwitcherItem[]; other: ProductSwitcherItem[]; - }> = combineLatest([ - this.organizationService.organizations$, - this.route.paramMap, - this.triggerProductUpdate$, - ]).pipe( + }> = combineLatest([this.organizations$, this.route.paramMap, this.triggerProductUpdate$]).pipe( map(([orgs, ...rest]): [Organization[], ParamMap, void] => { return [ // Sort orgs by name to match the order within the sidebar diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts index 8909df6cf8..d1ab7689cf 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts @@ -3,9 +3,12 @@ import { Component, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { Guid } from "@bitwarden/common/types/guid"; import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components"; @@ -39,10 +42,12 @@ export class RequestSMAccessComponent implements OnInit { private organizationService: OrganizationService, private smLandingApiService: SmLandingApiService, private toastService: ToastService, + private accountService: AccountService, ) {} async ngOnInit() { - this.organizations = (await this.organizationService.getAll()) + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organizations = (await firstValueFrom(this.organizationService.organizations$(userId))) .filter((e) => e.enabled) .sort((a, b) => a.name.localeCompare(b.name)); diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts index 3698031a5b..4d9dceab34 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts @@ -1,9 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { NoItemsModule, SearchModule } from "@bitwarden/components"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -22,10 +25,16 @@ export class SMLandingComponent implements OnInit { showSecretsManagerInformation: boolean = true; showGiveMembersAccessInstructions: boolean = false; - constructor(private organizationService: OrganizationService) {} + constructor( + private organizationService: OrganizationService, + private accountService: AccountService, + ) {} async ngOnInit() { - const enabledOrganizations = (await this.organizationService.getAll()).filter((e) => e.enabled); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const enabledOrganizations = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((e) => e.enabled); if (enabledOrganizations.length > 0) { this.handleEnabledOrganizations(enabledOrganizations); diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index b1a46bd13a..f78b292041 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -1,11 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; -import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs"; +import { BehaviorSubject, Observable, Subject, switchMap, takeUntil } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -45,10 +46,13 @@ export class CipherReportComponent implements OnDestroy { private modalService: ModalService, protected passwordRepromptService: PasswordRepromptService, protected organizationService: OrganizationService, + protected accountService: AccountService, protected i18nService: I18nService, private syncService: SyncService, ) { - this.organizations$ = this.organizationService.organizations$; + this.organizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ); this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { this.organizations = orgs; }); diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index 07dc218bd6..16541bdc10 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -7,7 +7,11 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("ExposedPasswordsReportComponent", () => { let auditService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); auditService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("ExposedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 13d2804c5e..1a0d4043b7 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -25,6 +26,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple protected cipherService: CipherService, protected auditService: AuditService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -35,6 +37,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 80760eb5de..385bda03f2 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -20,11 +24,14 @@ describe("InactiveTwoFactorReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -38,6 +45,10 @@ describe("InactiveTwoFactorReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 792ad0616f..52c52041c9 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -27,6 +28,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, private logService: LogService, passwordRepromptService: PasswordRepromptService, @@ -38,6 +40,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 9d16bbb1c6..6a26cd24fe 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -6,7 +6,11 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -19,11 +23,15 @@ describe("ReusedPasswordsReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); + accountService = mockAccountServiceWith(userId); + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -37,6 +45,10 @@ describe("ReusedPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index a8806acea1..a5c1c65560 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -24,6 +25,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -34,6 +36,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 5f66814fdf..7cd159108b 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -7,7 +7,11 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("UnsecuredWebsitesReportComponent", () => { let organizationService: MockProxy; let syncServiceMock: MockProxy; let collectionService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); collectionService = mock(); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -40,6 +47,10 @@ describe("UnsecuredWebsitesReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 6a1ba1f633..350e5c0398 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { CollectionService, Collection } from "@bitwarden/admin-console/common"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -22,6 +23,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl constructor( protected cipherService: CipherService, protected organizationService: OrganizationService, + accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -33,6 +35,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index bcace60ac0..578c220f39 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -6,8 +6,12 @@ import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,12 +25,15 @@ describe("WeakPasswordsReportComponent", () => { let passwordStrengthService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; beforeEach(() => { syncServiceMock = mock(); passwordStrengthService = mock(); organizationService = mock(); - organizationService.organizations$ = of([]); + organizationService.organizations$.mockReturnValue(of([])); + accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -44,6 +51,10 @@ describe("WeakPasswordsReportComponent", () => { provide: OrganizationService, useValue: organizationService, }, + { + provide: AccountService, + useValue: accountService, + }, { provide: ModalService, useValue: mock(), diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index f3ad6840c8..c374ecd0e4 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -32,6 +33,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen protected cipherService: CipherService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected organizationService: OrganizationService, + protected accountService: AccountService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, @@ -42,6 +44,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen modalService, passwordRepromptService, organizationService, + accountService, i18nService, syncService, ); diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 6141e983a6..a035202516 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -5,6 +5,7 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angula import { AbstractControl, FormBuilder, Validators } from "@angular/forms"; import { combineLatest, + firstValueFrom, map, Observable, of, @@ -24,8 +25,13 @@ import { CollectionResponse, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -110,6 +116,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private organizationUserApiService: OrganizationUserApiService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, + private accountService: AccountService, private toastService: ToastService, ) { this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; @@ -122,7 +129,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.formGroup.controls.selectedOrg.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((id) => this.loadOrg(id)); - this.organizations$ = this.organizationService.organizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + this.organizations$ = this.organizationService.organizations$(userId).pipe( first(), map((orgs) => orgs @@ -140,8 +150,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } async loadOrg(orgId: string) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const organization$ = this.organizationService - .get$(orgId) + .organizations$(userId) + .pipe(getOrganizationById(orgId)) .pipe(shareReplay({ refCount: true, bufferSize: 1 })); const groups$ = organization$.pipe( switchMap((organization) => { diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts index 1ca9b0de47..9127a213a4 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts @@ -49,7 +49,7 @@ describe("AddEditComponentV2", () => { } as Organization; organizationService = mock(); - organizationService.organizations$ = of([mockOrganization]); + organizationService.organizations$.mockReturnValue(of([mockOrganization])); policyService = mock(); policyService.policyAppliesToActiveUser$.mockImplementation((policyType: PolicyType) => diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts index 1dc15a7471..2deb5d3534 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component.ts @@ -8,6 +8,7 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Checkable, isChecked } from "@bitwarden/common/types/checkable"; @@ -76,7 +77,8 @@ export class BulkShareDialogComponent implements OnInit, OnDestroy { this.nonShareableCount = this.ciphers.length - this.shareableCiphers.length; const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.filter((c) => !c.readOnly); - this.organizations = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organizations = await firstValueFrom(this.organizationService.organizations$(userId)); if (this.organizationId == null && this.organizations.length > 0) { this.organizationId = this.organizations[0].id; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 6255ee11c4..37e3aca6cd 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -1,7 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { combineLatest, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatest, + firstValueFrom, + map, + Observable, + of, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { OrganizationUserApiService, @@ -15,7 +24,9 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -60,6 +71,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -67,16 +79,19 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)), ); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const managingOrg$ = this.configService .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) .pipe( switchMap((isAccountDeprovisioningEnabled) => isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map((organizations) => - organizations.find((o) => o.userIsManagedByOrganization === true), - ), - ) + ? this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => + organizations.find((o) => o.userIsManagedByOrganization === true), + ), + ) : of(null), ), ); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 47003d51ca..8d74f69ed0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -62,7 +62,7 @@ describe("vault filter service", () => { personalOwnershipPolicy = new ReplaySubject(1); singleOrgPolicy = new ReplaySubject(1); - organizationService.memberOrganizations$ = organizations; + organizationService.memberOrganizations$.mockReturnValue(organizations); folderService.folderViews$.mockReturnValue(folderViews); collectionService.decryptedCollections$ = collectionViews; policyService.policyAppliesToActiveUser$ diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index 97b44132e6..9b24f95e2e 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -48,8 +48,12 @@ const NestingDelimiter = "/"; export class VaultFilterService implements VaultFilterServiceAbstraction { private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + memberOrganizations$ = this.activeUserId$.pipe( + switchMap((id) => this.organizationService.memberOrganizations$(id)), + ); + organizationTree$: Observable> = combineLatest([ - this.organizationService.memberOrganizations$, + this.memberOrganizations$, this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), ]).pipe( diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 7c2f992456..464d074d9d 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -47,7 +47,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; @@ -193,7 +196,11 @@ export class VaultComponent implements OnInit, OnDestroy { private hasSubscription$ = new BehaviorSubject(false); private vaultItemDialogRef?: DialogRef | undefined; - private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( + private organizations$ = this.accountService.activeAccount$ + .pipe(map((a) => a?.id)) + .pipe(switchMap((id) => this.organizationService.organizations$(id))); + + private readonly unpaidSubscriptionDialog$ = this.organizations$.pipe( filter((organizations) => organizations.length === 1), map(([organization]) => organization), switchMap((organization) => @@ -212,9 +219,8 @@ export class VaultComponent implements OnInit, OnDestroy { ), ), ); - protected organizationsPaymentStatus$: Observable = combineLatest([ - this.organizationService.organizations$.pipe( + this.organizations$.pipe( map( (organizations) => organizations?.filter((org) => org.isOwner && org.canViewBillingHistory) ?? [], @@ -501,7 +507,7 @@ export class VaultComponent implements OnInit, OnDestroy { filter$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(this.activeUserId), allCollections$, - this.organizationService.organizations$, + this.organizations$, ciphers$, collections$, selectedCollection$, @@ -646,7 +652,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.messagingService.send("premiumRequired"); return; } else if (cipher.organizationId != null) { - const org = await this.organizationService.get(cipher.organizationId); + const org = await firstValueFrom( + this.organizations$.pipe(getOrganizationById(cipher.organizationId)), + ); if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) { this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId, @@ -971,7 +979,9 @@ export class VaultComponent implements OnInit, OnDestroy { } async deleteCollection(collection: CollectionView): Promise { - const organization = await this.organizationService.get(collection.organizationId); + const organization = await firstValueFrom( + this.organizations$.pipe(getOrganizationById(collection.organizationId)), + ); if (!collection.canDelete(organization)) { this.showMissingPermissionsError(); return; @@ -1136,9 +1146,7 @@ export class VaultComponent implements OnInit, OnDestroy { .filter((i) => i.cipher === undefined) .map((i) => i.collection.organizationId); const orgs = await firstValueFrom( - this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => orgIds.includes(o.id))), - ), + this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))), ); await this.bulkDelete(ciphers, collections, orgs); } diff --git a/apps/web/src/app/vault/individual-vault/view.component.spec.ts b/apps/web/src/app/vault/individual-vault/view.component.spec.ts index bde9f564c4..9bea7f14eb 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.spec.ts @@ -1,6 +1,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -11,7 +12,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -41,6 +43,8 @@ describe("ViewComponent", () => { const mockParams: ViewCipherDialogParams = { cipher: mockCipher, }; + const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -53,10 +57,14 @@ describe("ViewComponent", () => { { provide: CipherService, useValue: mock() }, { provide: ToastService, useValue: mock() }, { provide: MessagingService, useValue: mock() }, + { + provide: AccountService, + useValue: accountService, + }, { provide: LogService, useValue: mock() }, { provide: OrganizationService, - useValue: { get: jest.fn().mockResolvedValue(mockOrganization) }, + useValue: { organizations$: jest.fn().mockReturnValue(of([mockOrganization])) }, }, { provide: CollectionService, useValue: mock() }, { provide: FolderService, useValue: mock() }, diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index e9ca2bf8f8..3248073935 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -3,11 +3,13 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, firstValueFrom, map } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -94,6 +96,7 @@ export class ViewComponent implements OnInit { private toastService: ToastService, private organizationService: OrganizationService, private cipherAuthorizationService: CipherAuthorizationService, + private accountService: AccountService, ) {} /** @@ -103,8 +106,17 @@ export class ViewComponent implements OnInit { this.cipher = this.params.cipher; this.collections = this.params.collections; this.cipherTypeString = this.getCipherViewTypeString(); + + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (this.cipher.organizationId) { - this.organization = await this.organizationService.get(this.cipher.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe( + map((organizations) => organizations.find((o) => o.id === this.cipher.organizationId)), + ), + ); } this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index d9ba8af49f..4058c1151f 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -10,8 +10,12 @@ import { OrganizationUserApiService, CollectionView, } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -63,6 +67,7 @@ export class BulkCollectionsDialogComponent implements OnDestroy { private dialogRef: DialogRef, private formBuilder: FormBuilder, private organizationService: OrganizationService, + private accountService: AccountService, private groupService: GroupApiService, private organizationUserApiService: OrganizationUserApiService, private platformUtilsService: PlatformUtilsService, @@ -71,7 +76,13 @@ export class BulkCollectionsDialogComponent implements OnDestroy { private toastService: ToastService, ) { this.numCollections = this.params.collections.length; - const organization$ = this.organizationService.get$(this.params.organizationId); + const organization$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService + .organizations$(account?.id) + .pipe(getOrganizationById(this.params.organizationId)), + ), + ); const groups$ = organization$.pipe( switchMap((organization) => { if (!organization.useGroups) { diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index 25976c4fb8..c318e7389a 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -7,9 +7,11 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Account } from "../../../../../../../libs/importer/src/importers/lastpass/access/models"; import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "./admin-console-cipher-form-config.service"; @@ -50,8 +52,7 @@ describe("AdminConsoleCipherFormConfigService", () => { readOnly: false, } as CollectionAdminView; - const organization$ = new BehaviorSubject(testOrg as Organization); - const organizations$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); + const orgs$ = new BehaviorSubject([testOrg, testOrg2] as Organization[]); const getCipherAdmin = jest.fn().mockResolvedValue(null); const getCipher = jest.fn().mockResolvedValue(null); @@ -65,7 +66,7 @@ describe("AdminConsoleCipherFormConfigService", () => { TestBed.configureTestingModule({ providers: [ AdminConsoleCipherFormConfigService, - { provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } }, + { provide: OrganizationService, useValue: { organizations$: () => orgs$ } }, { provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([collection, collection2]) }, @@ -80,6 +81,10 @@ describe("AdminConsoleCipherFormConfigService", () => { }, { provide: ApiService, useValue: { getCipherAdmin } }, { provide: CipherService, useValue: { get: getCipher } }, + { + provide: AccountService, + useValue: { activeAccount$: new BehaviorSubject(new Account()) }, + }, ], }); adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index 0d3db55d3d..e3519d8fa3 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -31,6 +32,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); private cipherService: CipherService = inject(CipherService); private apiService: ApiService = inject(ApiService); + private accountService: AccountService = inject(AccountService); private allowPersonalOwnership$ = this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -41,12 +43,16 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ filter((filter) => filter !== undefined), ); - private allOrganizations$ = this.organizationService.organizations$.pipe( - map((orgs) => { - return orgs.filter( - (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, - ); - }), + private allOrganizations$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService.organizations$(account?.id).pipe( + map((orgs) => { + return orgs.filter( + (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, + ); + }), + ), + ), ); private organization$ = combineLatest([this.allOrganizations$, this.organizationId$]).pipe( diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index d28148c49d..7f118a48db 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -11,7 +11,6 @@ import { Unassigned, } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -90,7 +89,6 @@ export class VaultHeaderComponent implements OnInit { @Output() searchTextChanged = new EventEmitter(); protected CollectionDialogTabType = CollectionDialogTabType; - protected organizations$ = this.organizationService.organizations$; /** * Whether the extension refresh feature flag is enabled. @@ -101,7 +99,6 @@ export class VaultHeaderComponent implements OnInit { protected CipherType = CipherType; constructor( - private organizationService: OrganizationService, private i18nService: I18nService, private dialogService: DialogService, private collectionAdminService: CollectionAdminService, diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 779266d830..ca1d330ecf 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -49,6 +49,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; @@ -213,19 +214,24 @@ export class VaultComponent implements OnInit, OnDestroy { private resellerManagedOrgAlert: boolean; private vaultItemDialogRef?: DialogRef | undefined; - private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe( - filter((organizations) => organizations.length === 1), - map(([organization]) => organization), - switchMap((organization) => - from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( - tap((organizationMetaData) => { - this.hasSubscription$.next(organizationMetaData.hasSubscription); - }), - switchMap((organizationMetaData) => - from( - this.trialFlowService.handleUnpaidSubscriptionDialog( - organization, - organizationMetaData, + private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((id) => + this.organizationService.organizations$(id).pipe( + filter((organizations) => organizations.length === 1), + map(([organization]) => organization), + switchMap((organization) => + from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( + tap((organizationMetaData) => { + this.hasSubscription$.next(organizationMetaData.hasSubscription); + }), + switchMap((organizationMetaData) => + from( + this.trialFlowService.handleUnpaidSubscriptionDialog( + organization, + organizationMetaData, + ), + ), ), ), ), @@ -268,6 +274,7 @@ export class VaultComponent implements OnInit, OnDestroy { protected billingApiService: BillingApiServiceAbstraction, private organizationBillingService: OrganizationBillingServiceAbstraction, private resellerWarningService: ResellerWarningService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -292,10 +299,19 @@ export class VaultComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - const organization$ = organizationId$.pipe( - switchMap((organizationId) => this.organizationService.get$(organizationId)), - takeUntil(this.destroy$), - shareReplay({ refCount: false, bufferSize: 1 }), + const organization$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((id) => + organizationId$.pipe( + switchMap((organizationId) => + this.organizationService + .organizations$(id) + .pipe(map((organizations) => organizations.find((org) => org.id === organizationId))), + ), + takeUntil(this.destroy$), + shareReplay({ refCount: false, bufferSize: 1 }), + ), + ), ); const firstSetup$ = combineLatest([organization$, this.route.queryParams]).pipe( diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts index db63a3c1c6..dc78ebf7cd 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class ApproveAllCommand { constructor( private organizationAuthRequestService: OrganizationAuthRequestService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,13 @@ export class ApproveAllCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -58,6 +67,7 @@ export class ApproveAllCommand { return new ApproveAllCommand( serviceContainer.organizationAuthRequestService, serviceContainer.organizationService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts index 1c51f9397c..27597fd1a8 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts @@ -1,16 +1,19 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; export class ApproveCommand { constructor( - private organizationService: OrganizationService, + private organizationService: DefaultOrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountService: AccountService, ) {} async run(organizationId: string, id: string): Promise { @@ -30,7 +33,17 @@ export class ApproveCommand { return Response.badRequest("`" + id + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations?.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -57,6 +70,7 @@ export class ApproveCommand { return new ApproveCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts index 767acea99f..923545f15e 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class DenyAllCommand { constructor( private organizationService: OrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,17 @@ export class DenyAllCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -54,6 +67,7 @@ export class DenyAllCommand { return new DenyAllCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts index 87e633b2be..ac5c285f8d 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts @@ -1,8 +1,9 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -11,6 +12,7 @@ export class DenyCommand { constructor( private organizationService: OrganizationService, private organizationAuthRequestService: OrganizationAuthRequestService, + private accountServcie: AccountService, ) {} async run(organizationId: string, id: string): Promise { @@ -30,7 +32,17 @@ export class DenyCommand { return Response.badRequest("`" + id + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(this.accountServcie.activeAccount$.pipe(map((a) => a?.id))); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -57,6 +69,7 @@ export class DenyCommand { return new DenyCommand( serviceContainer.organizationService, serviceContainer.organizationAuthRequestService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts index 972be460df..31a0e74817 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -1,9 +1,11 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; import { ListResponse } from "@bitwarden/cli/models/response/list.response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ServiceContainer } from "../../service-container"; @@ -14,6 +16,7 @@ export class ListCommand { constructor( private organizationAuthRequestService: OrganizationAuthRequestService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} async run(organizationId: string): Promise { @@ -25,7 +28,17 @@ export class ListCommand { return Response.badRequest("`" + organizationId + "` is not a GUID."); } - const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (!userId) { + return Response.badRequest("No user found."); + } + + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(map((organizations) => organizations.find((o) => o.id === organizationId))), + ); if (!organization?.canManageUsersPassword) { return Response.error( "You do not have permission to approve pending device authorization requests.", @@ -46,6 +59,7 @@ export class ListCommand { return new ListCommand( serviceContainer.organizationAuthRequestService, serviceContainer.organizationService, + serviceContainer.accountService, ); } } diff --git a/bitwarden_license/bit-common/jest.config.js b/bitwarden_license/bit-common/jest.config.js index a0441b0188..ab31a4c26c 100644 --- a/bitwarden_license/bit-common/jest.config.js +++ b/bitwarden_license/bit-common/jest.config.js @@ -7,9 +7,17 @@ module.exports = { ...sharedConfig, displayName: "bit-common tests", testEnvironment: "jsdom", - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + { + "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/common": ["../../libs/common/src/*"], + "@bitwarden/admin-console/common": ["/libs/admin-console/src/common"], + ...(compilerOptions?.paths ?? {}), + }, + { + prefix: "/", + }, + ), setupFilesAfterEnv: ["/test.setup.ts"], transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$|@angular|rxjs|@bitwarden))"], moduleFileExtensions: ["ts", "js", "html", "mjs"], diff --git a/bitwarden_license/bit-web/jest.config.js b/bitwarden_license/bit-web/jest.config.js index 17b7139049..9c9c61b240 100644 --- a/bitwarden_license/bit-web/jest.config.js +++ b/bitwarden_license/bit-web/jest.config.js @@ -9,7 +9,15 @@ module.exports = { ...sharedConfig, preset: "jest-preset-angular", setupFilesAfterEnv: ["../../apps/web/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + { + "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/common": ["../../libs/common/src/*"], + "@bitwarden/admin-console/common": ["/libs/admin-console/src/common"], + ...(compilerOptions?.paths ?? {}), + }, + { + prefix: "/", + }, + ), }; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index f2f1658574..09195cd22f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -14,6 +14,8 @@ import { ProviderService } from "@bitwarden/common/admin-console/abstractions/pr import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlanType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -68,6 +70,7 @@ export class ClientsComponent { private apiService: ApiService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, + private accountService: AccountService, private activatedRoute: ActivatedRoute, private dialogService: DialogService, private i18nService: I18nService, @@ -136,13 +139,14 @@ export class ClientsComponent { async load() { const response = await this.apiService.getProviderClients(this.providerId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const clients = response.data != null && response.data.length > 0 ? response.data : []; this.dataSource.data = clients; this.manageOrganizations = (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; - const candidateOrgs = (await this.organizationService.getAll()).filter( - (o) => o.isOwner && o.providerId == null, - ); + const candidateOrgs = ( + await firstValueFrom(this.organizationService.organizations$(userId)) + ).filter((o) => o.isOwner && o.providerId == null); const allowedOrgsIds = await Promise.all( candidateOrgs.map((o) => this.organizationApiService.get(o.id)), ).then((orgs) => diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 6449ef7a70..779cb96146 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -9,13 +9,17 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Observable, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MemberDecryptionType, OpenIdConnectRedirectBehavior, @@ -28,6 +32,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -195,6 +200,7 @@ export class SsoComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, private organizationApiService: OrganizationApiServiceAbstraction, private configService: ConfigService, private toastService: ToastService, @@ -260,7 +266,12 @@ export class SsoComponent implements OnInit, OnDestroy { } async load() { - this.organization = await this.organizationService.get(this.organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); const ssoSettings = await this.organizationApiService.getSso(this.organizationId); this.populateForm(ssoSettings); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts index a1f7564156..d042c4d904 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm-org-enabled.guard.ts @@ -1,7 +1,13 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -10,13 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv export const organizationEnabledGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const org = await orgService.get(route.params.organizationId); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const org = await firstValueFrom( + orgService.organizations$(userId).pipe(getOrganizationById(route.params.organizationId)), + ); if (org == null || !org.canAccessSecretsManager) { return createUrlTreeFromSnapshot(route, ["/"]); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts index 2a36bf1cbb..39576bc8dc 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/guards/sm.guard.ts @@ -5,8 +5,11 @@ import { createUrlTreeFromSnapshot, RouterStateSnapshot, } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; /** @@ -18,13 +21,15 @@ export const canActivateSM: CanActivateFn = async ( ) => { const syncService = inject(SyncService); const orgService = inject(OrganizationService); + const accountService = inject(AccountService); /** Workaround to avoid service initialization race condition. */ if ((await syncService.getLastSync()) == null) { await syncService.fullSync(false); } - const orgs = await orgService.getAll(); + const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); + const orgs = await firstValueFrom(orgService.organizations$(userId)); const smOrg = orgs.find((o) => o.canAccessSecretsManager); if (smOrg) { return createUrlTreeFromSnapshot(route, ["/sm", smOrg.id]); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index adf01afd10..6594b71a14 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -15,8 +15,13 @@ import { takeUntil, } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo"; import { OrganizationCounts } from "../models/view/counts.view"; @@ -41,6 +46,7 @@ export class NavigationComponent implements OnInit, OnDestroy { constructor( protected route: ActivatedRoute, private organizationService: OrganizationService, + private accountService: AccountService, private countService: CountService, private projectService: ProjectService, private secretService: SecretService, @@ -50,7 +56,15 @@ export class NavigationComponent implements OnInit, OnDestroy { ngOnInit() { const org$ = this.route.params.pipe( - concatMap((params) => this.organizationService.get(params.organizationId)), + concatMap((params) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), distinctUntilChanged(), takeUntil(this.destroy$), ); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index a95192e0d9..cb3f601676 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -20,8 +20,13 @@ import { import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -112,6 +117,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private serviceAccountService: ServiceAccountService, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private smOnboardingTasksService: SMOnboardingTasksService, @@ -130,7 +136,15 @@ export class OverviewComponent implements OnInit, OnDestroy { distinctUntilChanged(), ); - const org$ = orgId$.pipe(switchMap((orgId) => this.organizationService.get(orgId))); + const org$ = orgId$.pipe( + switchMap((orgId) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService.organizations$(userId).pipe(getOrganizationById(orgId)), + ), + ), + ), + ); org$.pipe(takeUntil(this.destroy$)).subscribe((org) => { this.organizationId = org.id; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts index 78d367776d..159b90d543 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts @@ -3,10 +3,15 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { RouterService } from "@bitwarden/web-vault/app/core"; @@ -32,6 +37,8 @@ describe("Project Redirect Guard", () => { let i18nServiceMock: MockProxy; let toastService: MockProxy; let router: Router; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; const projectView = { @@ -50,6 +57,7 @@ describe("Project Redirect Guard", () => { projectServiceMock = mock(); i18nServiceMock = mock(); toastService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ imports: [ @@ -71,6 +79,7 @@ describe("Project Redirect Guard", () => { ], providers: [ { provide: OrganizationService, useValue: organizationService }, + { provide: AccountService, useValue: accountService }, { provide: RouterService, useValue: routerService }, { provide: ProjectService, useValue: projectServiceMock }, { provide: I18nService, useValue: i18nServiceMock }, @@ -83,7 +92,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView)); // Act @@ -95,7 +104,7 @@ describe("Project Redirect Guard", () => { it("redirects to sm/projects if project does not exist", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); // Act await router.navigateByUrl("sm/123/projects/124"); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index b9c6d86cef..f4950bf53f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -2,9 +2,22 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs"; +import { + combineLatest, + combineLatestWith, + filter, + firstValueFrom, + Observable, + startWith, + switchMap, +} from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -49,6 +62,7 @@ export class ProjectSecretsComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, private logService: LogService, ) {} @@ -71,8 +85,13 @@ export class ProjectSecretsComponent implements OnInit { switchMap(async ([_, params]) => { this.organizationId = params.organizationId; this.projectId = params.projectId; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizationEnabled = ( - await this.organizationService.get(params.organizationId) + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) )?.enabled; return await this.getSecretsByProject(); }), diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts index 08c9446e48..9e60cf0535 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts @@ -14,7 +14,12 @@ import { concatMap, } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DialogService } from "@bitwarden/components"; import { ProjectCounts } from "../../models/view/counts.view"; @@ -49,6 +54,7 @@ export class ProjectComponent implements OnInit, OnDestroy { private accessPolicyService: AccessPolicyService, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, private countService: CountService, ) {} @@ -65,7 +71,15 @@ export class ProjectComponent implements OnInit, OnDestroy { const projectId$ = this.route.params.pipe(map((p) => p.projectId)); const organization$ = this.route.params.pipe( - concatMap((params) => this.organizationService.get$(params.organizationId)), + concatMap((params) => + getUserId(this.accountService.activeAccount$).pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ), + ), + ), ); const projectCounts$ = combineLatest([ this.route.params, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index 8ced29fa08..e09aaea6e2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -2,9 +2,21 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, lastValueFrom, Observable, startWith, switchMap } from "rxjs"; +import { + combineLatest, + firstValueFrom, + lastValueFrom, + Observable, + startWith, + switchMap, +} from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DialogService } from "@bitwarden/components"; import { ProjectListView } from "../../models/view/project-list.view"; @@ -41,6 +53,7 @@ export class ProjectsComponent implements OnInit { private projectService: ProjectService, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} ngOnInit() { @@ -50,8 +63,13 @@ export class ProjectsComponent implements OnInit { ]).pipe( switchMap(async ([params]) => { this.organizationId = params.organizationId; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizationEnabled = ( - await this.organizationService.get(params.organizationId) + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) )?.enabled; return await this.getProjects(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index c12930bb04..680dda72af 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -3,9 +3,14 @@ import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { lastValueFrom, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -97,6 +102,7 @@ export class SecretDialogComponent implements OnInit, OnDestroy { private projectService: ProjectService, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, private accessPolicyService: AccessPolicyService, private accessPolicySelectorService: AccessPolicySelectorService, private toastService: ToastService, @@ -127,7 +133,16 @@ export class SecretDialogComponent implements OnInit, OnDestroy { await this.loadAddDialog(); } - if ((await this.organizationService.get(this.data.organizationId))?.isAdmin) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if ( + ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.data.organizationId)), + ) + )?.isAdmin + ) { this.formGroup.get("project").removeValidators(Validators.required); this.formGroup.get("project").updateValueAndValidity(); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index 5d1ed78081..b58173a1cc 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -2,9 +2,14 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; +import { combineLatestWith, firstValueFrom, Observable, startWith, switchMap } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -46,6 +51,7 @@ export class SecretsComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, private logService: LogService, ) {} @@ -55,8 +61,13 @@ export class SecretsComponent implements OnInit { combineLatestWith(this.route.params), switchMap(async ([_, params]) => { this.organizationId = params.organizationId; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizationEnabled = ( - await this.organizationService.get(params.organizationId) + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) )?.enabled; return await this.getSecrets(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts index a4079a6def..4301b5ae81 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts @@ -3,10 +3,15 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { RouterService } from "@bitwarden/web-vault/app/core"; @@ -32,6 +37,8 @@ describe("Service account Redirect Guard", () => { let i18nServiceMock: MockProxy; let toastService: MockProxy; let router: Router; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; const serviceAccountView = { @@ -46,6 +53,7 @@ describe("Service account Redirect Guard", () => { serviceAccountServiceMock = mock(); i18nServiceMock = mock(); toastService = mock(); + accountService = mockAccountServiceWith(userId); TestBed.configureTestingModule({ imports: [ @@ -67,6 +75,7 @@ describe("Service account Redirect Guard", () => { ], providers: [ { provide: OrganizationService, useValue: organizationService }, + { provide: AccountService, useValue: accountService }, { provide: RouterService, useValue: routerService }, { provide: ServiceAccountService, useValue: serviceAccountServiceMock }, { provide: I18nService, useValue: i18nServiceMock }, @@ -79,7 +88,7 @@ describe("Service account Redirect Guard", () => { it("redirects to sm/{orgId}/machine-accounts/{serviceAccountId} if machine account exists", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); serviceAccountServiceMock.getByServiceAccountId.mockReturnValue( Promise.resolve(serviceAccountView), ); @@ -93,7 +102,7 @@ describe("Service account Redirect Guard", () => { it("redirects to sm/machine-accounts if machine account does not exist", async () => { // Arrange - organizationService.getAll.mockResolvedValue([smOrg1]); + organizationService.organizations$.mockReturnValue(of([smOrg1])); // Act await router.navigateByUrl("sm/123/machine-accounts/124"); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts index b134cb94cd..7deaefae82 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts @@ -2,9 +2,14 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, Observable, startWith, switchMap } from "rxjs"; +import { combineLatest, firstValueFrom, Observable, startWith, switchMap } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DialogService } from "@bitwarden/components"; import { @@ -39,6 +44,7 @@ export class ServiceAccountsComponent implements OnInit { private dialogService: DialogService, private serviceAccountService: ServiceAccountService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} ngOnInit() { @@ -48,8 +54,13 @@ export class ServiceAccountsComponent implements OnInit { ]).pipe( switchMap(async ([params]) => { this.organizationId = params.organizationId; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizationEnabled = ( - await this.organizationService.get(params.organizationId) + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ) )?.enabled; return await this.getServiceAccounts(); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts index d607eabbe0..266962c826 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-export.component.ts @@ -5,7 +5,12 @@ import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -40,6 +45,7 @@ export class SecretsManagerExportComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private i18nService: I18nService, private organizationService: OrganizationService, + private accountService: AccountService, private platformUtilsService: PlatformUtilsService, private smPortingService: SecretsManagerPortingService, private fileDownloadService: FileDownloadService, @@ -52,7 +58,14 @@ export class SecretsManagerExportComponent implements OnInit, OnDestroy { async ngOnInit() { this.route.params .pipe( - switchMap(async (params) => await this.organizationService.get(params.organizationId)), + switchMap(async (params) => { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + return await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); + }), takeUntil(this.destroy$), ) .subscribe((organization) => { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts index 05b677d490..9e3ca522bc 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts @@ -1,8 +1,13 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { AccessPolicySelectorService } from "./access-policy-selector.service"; import { ApItemValueType } from "./models/ap-item-value.type"; @@ -12,13 +17,16 @@ import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; describe("AccessPolicySelectorService", () => { let organizationService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; let sut: AccessPolicySelectorService; beforeEach(() => { organizationService = mock(); + accountService = mockAccountServiceWith(userId); - sut = new AccessPolicySelectorService(organizationService); + sut = new AccessPolicySelectorService(organizationService, accountService as AccountService); }); afterEach(() => jest.resetAllMocks()); @@ -26,7 +34,7 @@ describe("AccessPolicySelectorService", () => { describe("showAccessRemovalWarning", () => { it("returns false when current user is admin", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = []; @@ -38,7 +46,7 @@ describe("AccessPolicySelectorService", () => { it("returns false when current user is owner", async () => { const org = orgFactory(); org.type = OrganizationUserType.Owner; - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = []; @@ -49,7 +57,7 @@ describe("AccessPolicySelectorService", () => { it("returns true when current user isn't owner/admin and all policies are removed", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = []; @@ -60,7 +68,7 @@ describe("AccessPolicySelectorService", () => { it("returns true when current user isn't owner/admin and user policy is set to canRead", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = []; selectedPolicyValues.push( @@ -79,7 +87,7 @@ describe("AccessPolicySelectorService", () => { it("returns false when current user isn't owner/admin and user policy is set to canReadWrite", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = [ createApItemValueType( @@ -97,7 +105,7 @@ describe("AccessPolicySelectorService", () => { it("returns true when current user isn't owner/admin and a group Read policy is submitted that the user is a member of", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = [ createApItemValueType( @@ -118,7 +126,7 @@ describe("AccessPolicySelectorService", () => { it("returns false when current user isn't owner/admin and a group ReadWrite policy is submitted that the user is a member of", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = [ createApItemValueType( @@ -139,7 +147,7 @@ describe("AccessPolicySelectorService", () => { it("returns true when current user isn't owner/admin and a group ReadWrite policy is submitted that the user is not a member of", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = [ createApItemValueType( @@ -160,7 +168,7 @@ describe("AccessPolicySelectorService", () => { it("returns false when current user isn't owner/admin, user policy is set to CanRead, and user is in read write group", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = [ createApItemValueType( @@ -187,7 +195,7 @@ describe("AccessPolicySelectorService", () => { it("returns true when current user isn't owner/admin, user policy is set to CanRead, and user is not in ReadWrite group", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = [ createApItemValueType( @@ -214,7 +222,7 @@ describe("AccessPolicySelectorService", () => { it("returns true when current user isn't owner/admin, user policy is set to CanRead, and user is in Read group", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const selectedPolicyValues: ApItemValueType[] = [ createApItemValueType( @@ -242,7 +250,7 @@ describe("AccessPolicySelectorService", () => { describe("showSecretAccessRemovalWarning", () => { it("returns false when there are no current access policies", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = []; const selectedPolicyValues: ApItemValueType[] = []; @@ -257,7 +265,7 @@ describe("AccessPolicySelectorService", () => { }); it("returns false when current user is admin", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( @@ -281,7 +289,7 @@ describe("AccessPolicySelectorService", () => { it("returns false when current user is owner", async () => { const org = orgFactory(); org.type = OrganizationUserType.Owner; - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( @@ -304,7 +312,7 @@ describe("AccessPolicySelectorService", () => { }); it("returns false when current non-admin user doesn't have Read, Write access with current access policies -- user policy", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( @@ -327,7 +335,7 @@ describe("AccessPolicySelectorService", () => { }); it("returns false when current non-admin user doesn't have Read, Write access with current access policies -- group policy", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( @@ -352,7 +360,7 @@ describe("AccessPolicySelectorService", () => { }); it("returns true when current non-admin user has Read, Write access with current access policies and doesn't with selected -- user policy", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( @@ -381,7 +389,7 @@ describe("AccessPolicySelectorService", () => { }); it("returns true when current non-admin user has Read, Write access with current access policies and doesn't with selected -- group policy", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( @@ -415,7 +423,7 @@ describe("AccessPolicySelectorService", () => { }); it("returns false when current non-admin user has Read, Write access with current access policies and does with selected -- user policy", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( @@ -446,7 +454,7 @@ describe("AccessPolicySelectorService", () => { }); it("returns false when current non-admin user has Read, Write access with current access policies and does with selected -- group policy", async () => { const org = setupUserOrg(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.organizations$.calledWith(userId).mockReturnValue(of([org])); const currentAccessPolicies: ApItemViewType[] = [ createApItemViewType( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts index a4d1c3cd03..7fbf2bc01a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts @@ -1,6 +1,12 @@ import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ApItemValueType } from "./models/ap-item-value.type"; import { ApItemViewType } from "./models/ap-item-view.type"; @@ -11,13 +17,22 @@ import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; providedIn: "root", }) export class AccessPolicySelectorService { - constructor(private organizationService: OrganizationService) {} + constructor( + private organizationService: OrganizationService, + private accountServcie: AccountService, + ) {} async showAccessRemovalWarning( organizationId: string, selectedPoliciesValues: ApItemValueType[], ): Promise { - const organization = await this.organizationService.get(organizationId); + const userId = await firstValueFrom(getUserId(this.accountServcie.activeAccount$)); + const organization = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), + ); + if (!organization) { + return false; + } if (organization.isOwner || organization.isAdmin) { return false; } @@ -38,7 +53,13 @@ export class AccessPolicySelectorService { return false; } - const organization = await this.organizationService.get(organizationId); + const userId = await firstValueFrom(getUserId(this.accountServcie.activeAccount$)); + const organization = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), + ); + if (!organization) { + return false; + } if (organization.isOwner || organization.isAdmin || !this.userHasReadWriteAccess(current)) { return false; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts index 8b046b7ff9..b0a481d077 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.ts @@ -2,9 +2,14 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Subject, takeUntil, concatMap } from "rxjs"; +import { Subject, takeUntil, concatMap, firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DialogService } from "@bitwarden/components"; import { @@ -33,12 +38,20 @@ export class NewMenuComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private dialogService: DialogService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} ngOnInit() { this.route.params .pipe( - concatMap(async (params) => await this.organizationService.get(params.organizationId)), + concatMap(async (params) => { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + return await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); + }), takeUntil(this.destroy$), ) .subscribe((org) => { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts index e0293ba531..2eb9d6017b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/org-suspended.component.ts @@ -1,8 +1,13 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { map, concatMap } from "rxjs"; +import { map, concatMap, firstValueFrom } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Icon, Icons } from "@bitwarden/components"; @Component({ @@ -11,12 +16,20 @@ import { Icon, Icons } from "@bitwarden/components"; export class OrgSuspendedComponent { constructor( private organizationService: OrganizationService, + private accountService: AccountService, private route: ActivatedRoute, ) {} protected NoAccess: Icon = Icons.NoAccess; protected organizationName$ = this.route.params.pipe( - concatMap(async (params) => await this.organizationService.get(params.organizationId)), + concatMap(async (params) => { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + return await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(params.organizationId)), + ); + }), map((org) => org?.name), ); } diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts index 627495a6bd..8d8112587c 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts @@ -2,7 +2,7 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, debounceTime, map, Observable, of, skipWhile } from "rxjs"; +import { combineLatest, debounceTime, firstValueFrom, map, Observable, of, skipWhile } from "rxjs"; import { CriticalAppsService, @@ -14,8 +14,13 @@ import { ApplicationHealthReportDetailWithCriticalFlag, ApplicationHealthReportSummary, } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -77,34 +82,42 @@ export class AllApplicationsComponent implements OnInit { ); const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; - combineLatest([ - this.dataService.applications$, - this.criticalAppsService.getAppsListForOrg(organizationId), - this.organizationService.get$(organizationId), - ]) - .pipe( - takeUntilDestroyed(this.destroyRef), - skipWhile(([_, __, organization]) => !organization), - map(([applications, criticalApps, organization]) => { - const criticalUrls = criticalApps.map((ca) => ca.uri); - const data = applications?.map((app) => ({ - ...app, - isMarkedAsCritical: criticalUrls.includes(app.applicationName), - })) as ApplicationHealthReportDetailWithCriticalFlag[]; - return { data, organization }; - }), - ) - .subscribe(({ data, organization }) => { - if (data) { - this.dataSource.data = data; - this.applicationSummary = this.reportService.generateApplicationsSummary(data); - } - if (organization) { - this.organization = organization; - } - }); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - this.isLoading$ = this.dataService.isLoading$; + if (organizationId) { + const organization$ = this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(organizationId)); + + combineLatest([ + this.dataService.applications$, + this.criticalAppsService.getAppsListForOrg(organizationId), + organization$, + ]) + .pipe( + takeUntilDestroyed(this.destroyRef), + skipWhile(([_, __, organization]) => !organization), + map(([applications, criticalApps, organization]) => { + const criticalUrls = criticalApps.map((ca) => ca.uri); + const data = applications?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as ApplicationHealthReportDetailWithCriticalFlag[]; + return { data, organization }; + }), + ) + .subscribe(({ data, organization }) => { + if (data) { + this.dataSource.data = data; + this.applicationSummary = this.reportService.generateApplicationsSummary(data); + } + if (organization) { + this.organization = organization; + } + }); + + this.isLoading$ = this.dataService.isLoading$; + } } constructor( @@ -116,6 +129,7 @@ export class AllApplicationsComponent implements OnInit { protected dataService: RiskInsightsDataService, protected organizationService: OrganizationService, protected reportService: RiskInsightsReportService, + private accountService: AccountService, protected criticalAppsService: CriticalAppsService, protected dialogService: DialogService, ) { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts index 1e9e4171bc..852e9adafb 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health-members-uri.component.spec.ts @@ -10,8 +10,12 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; @@ -24,6 +28,7 @@ describe("PasswordHealthMembersUriComponent", () => { let fixture: ComponentFixture; let cipherServiceMock: MockProxy; const passwordHealthServiceMock = mock(); + const userId = Utils.newGuid() as UserId; const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); @@ -36,6 +41,7 @@ describe("PasswordHealthMembersUriComponent", () => { { provide: I18nService, useValue: mock() }, { provide: AuditService, useValue: mock() }, { provide: OrganizationService, useValue: mock() }, + { provide: AccountService, useValue: mockAccountServiceWith(userId) }, { provide: PasswordStrengthServiceAbstraction, useValue: mock(), diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 0b19935985..52a22ac294 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -63,7 +63,15 @@ export class CollectionsComponent implements OnInit { } if (this.organization == null) { - this.organization = await this.organizationService.get(this.cipher.organizationId); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(activeUserId) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.cipher.organizationId), + ), + ), + ); } } diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts index cebd81846c..edb233cc76 100644 --- a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -3,14 +3,14 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -105,7 +105,16 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 20.0, }); - this.organization = await this.organizationService.get(this.dialogParams.organizationId); + this.user = await firstValueFrom(this.accountService.activeAccount$); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(this.user.id) + .pipe( + map((organizations) => + organizations.find((org) => org.id === this.dialogParams.organizationId), + ), + ), + ); payPalCustomField = "organization_id:" + this.organization.id; this.payPalConfig.subject = this.organization.name; } else if (this.dialogParams.providerId) { @@ -119,7 +128,6 @@ export class AddAccountCreditDialogComponent implements OnInit { this.formGroup.patchValue({ creditAmount: 10.0, }); - this.user = await firstValueFrom(this.accountService.activeAccount$); payPalCustomField = "user_id:" + this.user.id; this.payPalConfig.subject = this.user.email; } diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 37dec53b9c..534a1337ed 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -54,7 +54,11 @@ export class ShareComponent implements OnInit, OnDestroy { const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); - this.organizations$ = this.organizationService.memberOrganizations$.pipe( + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + + this.organizations$ = this.organizationService.memberOrganizations$(userId).pipe( map((orgs) => { return orgs .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 7b6123e258..766f0a2d41 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -67,8 +67,8 @@ import { } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service"; import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service"; import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service"; @@ -992,13 +992,14 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: InternalOrganizationServiceAbstraction, - useClass: OrganizationService, + useClass: DefaultOrganizationService, deps: [StateProvider], }), safeProvider({ provide: OrganizationServiceAbstraction, useExisting: InternalOrganizationServiceAbstraction, }), + safeProvider({ provide: OrganizationUserApiService, useClass: DefaultOrganizationUserApiService, @@ -1394,7 +1395,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CipherAuthorizationService, useClass: DefaultCipherAuthorizationService, - deps: [CollectionService, OrganizationServiceAbstraction], + deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction], }), safeProvider({ provide: AuthRequestApiService, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index b86d48d391..26f645d89e 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -7,10 +7,7 @@ import { concatMap, firstValueFrom, map, Observable, Subject, takeUntil } from " import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +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 { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -235,9 +232,12 @@ export class AddEditComponent implements OnInit, OnDestroy { this.ownershipOptions.push({ name: myEmail, value: null }); } - const orgs = await this.organizationService.getAll(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); orgs - .filter(isMember) + .filter((org) => org.isMember) .sort(Utils.getSortFunction(this.i18nService, "name")) .forEach((o) => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { @@ -313,10 +313,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } // Only Admins can clone a cipher to different owner if (this.cloneMode && this.cipher.organizationId != null) { - const cipherOrg = (await firstValueFrom(this.organizationService.memberOrganizations$)).find( - (o) => o.id === this.cipher.organizationId, + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + const cipherOrg = ( + await firstValueFrom(this.organizationService.memberOrganizations$(activeUserId)) + ).find((o) => o.id === this.cipher.organizationId); + if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) { this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }]; } @@ -658,7 +662,13 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.collections.length === 1) { (this.collections[0] as any).checked = true; } - const org = await this.organizationService.get(this.cipher.organizationId); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const org = ( + await firstValueFrom(this.organizationService.organizations$(activeUserId)) + ).find((org) => org.id === this.cipher.organizationId); if (org != null) { this.cipher.organizationUseTotp = org.useTotp; } diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index 260780e196..96fb74ba96 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -4,14 +4,12 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, from, map, mergeMap, Observable, switchMap } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +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"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -56,9 +54,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async buildOrganizations(): Promise { - let organizations = await this.organizationService.getAll(); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + let organizations = await firstValueFrom(this.organizationService.organizations$(userId)); if (organizations != null) { - organizations = organizations.filter(isMember).sort((a, b) => a.name.localeCompare(b.name)); + organizations = organizations + .filter((o) => o.isMember) + .sort((a, b) => a.name.localeCompare(b.name)); } return organizations; diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index da81f340fd..05c214ece1 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -57,14 +57,6 @@ export function getOrganizationById(id: string) { return map((orgs) => orgs.find((o) => o.id === id)); } -/** - * Returns `true` if a user is a member of an organization (rather than only being a ProviderUser) - * @deprecated Use organizationService.organizations$ with a filter instead - */ -export function isMember(org: Organization): boolean { - return org.isMember; -} - /** * Publishes an observable stream of organizations. This service is meant to * be used widely across Bitwarden as the primary way of fetching organizations. @@ -73,41 +65,23 @@ export function isMember(org: Organization): boolean { */ export abstract class OrganizationService { /** - * Publishes state for all organizations under the active user. + * Publishes state for all organizations under the specified user. * @returns An observable list of organizations */ - organizations$: Observable; + organizations$: (userId: UserId) => Observable; // @todo Clean these up. Continuing to expand them is not recommended. // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: Observable; - /** - * @deprecated This is currently only used in the CLI, and should not be - * used in any new calls. Use get$ instead for the time being, and we'll be - * removing this method soon. See Jira for details: - * https://bitwarden.atlassian.net/browse/AC-2252. - */ - getFromState: (id: string) => Promise; + memberOrganizations$: (userId: UserId) => Observable; /** * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. */ - canManageSponsorships$: Observable; + canManageSponsorships$: (userId: UserId) => Observable; /** * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. */ - familySponsorshipAvailable$: Observable; - hasOrganizations: () => Promise; - get$: (id: string) => Observable; - get: (id: string) => Promise; - /** - * @deprecated This method is only used in key connector and will be removed soon as part of https://bitwarden.atlassian.net/browse/AC-2252. - */ - getAll: (userId?: string) => Promise; - - /** - * Publishes state for all organizations for the given user id or the active user. - */ - getAll$: (userId?: UserId) => Observable; + familySponsorshipAvailable$: (userId: UserId) => Observable; + hasOrganizations: (userId: UserId) => Observable; } /** @@ -120,20 +94,18 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio /** * Replaces state for the provided organization, or creates it if not found. * @param organization The organization state being saved. - * @param userId The userId to replace state for. Defaults to the active - * user. + * @param userId The userId to replace state for. */ - upsert: (OrganizationData: OrganizationData) => Promise; + upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; /** - * Replaces state for the entire registered organization list for the active user. + * Replaces state for the entire registered organization list for the specified user. * You probably don't want this unless you're calling from a full sync * operation or a logout. See `upsert` for creating & updating a single * organization in the state. - * @param organizations A complete list of all organization state for the active - * user. - * @param userId The userId to replace state for. Defaults to the active + * @param organizations A complete list of all organization state for the provided * user. + * @param userId The userId to replace state for. */ - replace: (organizations: { [id: string]: OrganizationData }, userId?: UserId) => Promise; + replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts b/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts deleted file mode 100644 index c25a153a06..0000000000 --- a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -export function canAccessVaultTab(org: Organization): boolean { - return org.canViewAllCollections; -} - -export function canAccessSettingsTab(org: Organization): boolean { - return ( - org.isOwner || - org.canManagePolicies || - org.canManageSso || - org.canManageScim || - org.canAccessImport || - org.canAccessExport || - org.canManageDeviceApprovals - ); -} - -export function canAccessMembersTab(org: Organization): boolean { - return org.canManageUsers || org.canManageUsersPassword; -} - -export function canAccessGroupsTab(org: Organization): boolean { - return org.canManageGroups; -} - -export function canAccessReportingTab(org: Organization): boolean { - return org.canAccessReports || org.canAccessEventLogs; -} - -export function canAccessBillingTab(org: Organization): boolean { - return org.isOwner; -} - -export function canAccessOrgAdmin(org: Organization): boolean { - // Admin console can only be accessed by Owners for disabled organizations - if (!org.enabled && !org.isOwner) { - return false; - } - return ( - canAccessMembersTab(org) || - canAccessGroupsTab(org) || - canAccessReportingTab(org) || - canAccessBillingTab(org) || - canAccessSettingsTab(org) || - canAccessVaultTab(org) - ); -} - -export function getOrganizationById(id: string) { - return map((orgs) => orgs.find((o) => o.id === id)); -} - -/** - * Publishes an observable stream of organizations. This service is meant to - * be used widely across Bitwarden as the primary way of fetching organizations. - * Risky operations like updates are isolated to the - * internal extension `InternalOrganizationServiceAbstraction`. - */ -export abstract class vNextOrganizationService { - /** - * Publishes state for all organizations under the specified user. - * @returns An observable list of organizations - */ - organizations$: (userId: UserId) => Observable; - - // @todo Clean these up. Continuing to expand them is not recommended. - // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: (userId: UserId) => Observable; - /** - * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. - */ - canManageSponsorships$: (userId: UserId) => Observable; - /** - * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. - */ - familySponsorshipAvailable$: (userId: UserId) => Observable; - hasOrganizations: (userId: UserId) => Observable; -} - -/** - * Big scary buttons that **update** organization state. These should only be - * called from within admin-console scoped code. Extends the base - * `OrganizationService` for easy access to `get` calls. - * @internal - */ -export abstract class vNextInternalOrganizationServiceAbstraction extends vNextOrganizationService { - /** - * Replaces state for the provided organization, or creates it if not found. - * @param organization The organization state being saved. - * @param userId The userId to replace state for. - */ - upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; - - /** - * Replaces state for the entire registered organization list for the specified user. - * You probably don't want this unless you're calling from a full sync - * operation or a logout. See `upsert` for creating & updating a single - * organization in the state. - * @param organizations A complete list of all organization state for the provided - * user. - * @param userId The userId to replace state for. - */ - replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; -} diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index da9a82e7c5..8a49109f34 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -1,6 +1,6 @@ import { ProductTierType } from "../../../billing/enums/product-tier-type.enum"; import { OrganizationUserStatusType, OrganizationUserType } from "../../enums"; -import { ORGANIZATIONS } from "../../services/organization/organization.service"; +import { ORGANIZATIONS } from "../../services/organization/organization.state"; import { OrganizationData } from "./organization.data"; diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts similarity index 96% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.spec.ts index 9e2ea3a459..41c89c0e41 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.spec.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.spec.ts @@ -6,11 +6,11 @@ import { OrganizationId, UserId } from "../../../types/guid"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { DefaultvNextOrganizationService } from "./default-vnext-organization.service"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { DefaultOrganizationService } from "./default-organization.service"; +import { ORGANIZATIONS } from "./organization.state"; describe("OrganizationService", () => { - let organizationService: DefaultvNextOrganizationService; + let organizationService: DefaultOrganizationService; const fakeUserId = Utils.newGuid() as UserId; let fakeStateProvider: FakeStateProvider; @@ -86,7 +86,7 @@ describe("OrganizationService", () => { beforeEach(async () => { fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(fakeUserId)); - organizationService = new DefaultvNextOrganizationService(fakeStateProvider); + organizationService = new DefaultOrganizationService(fakeStateProvider); }); describe("canManageSponsorships", () => { diff --git a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts b/libs/common/src/admin-console/services/organization/default-organization.service.ts similarity index 92% rename from libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts rename to libs/common/src/admin-console/services/organization/default-organization.service.ts index 8b73c271da..e78136455f 100644 --- a/libs/common/src/admin-console/services/organization/default-vnext-organization.service.ts +++ b/libs/common/src/admin-console/services/organization/default-organization.service.ts @@ -4,11 +4,11 @@ import { map, Observable } from "rxjs"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; -import { vNextInternalOrganizationServiceAbstraction } from "../../abstractions/organization/vnext.organization.service"; +import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../models/data/organization.data"; import { Organization } from "../../models/domain/organization"; -import { ORGANIZATIONS } from "./vnext-organization.state"; +import { ORGANIZATIONS } from "./organization.state"; /** * Filter out organizations from an observable that __do not__ offer a @@ -41,9 +41,7 @@ function mapToBooleanHasAnyOrganizations() { return map((orgs) => orgs.length > 0); } -export class DefaultvNextOrganizationService - implements vNextInternalOrganizationServiceAbstraction -{ +export class DefaultOrganizationService implements InternalOrganizationServiceAbstraction { memberOrganizations$(userId: UserId): Observable { return this.organizations$(userId).pipe(mapToExcludeProviderOrganizations()); } diff --git a/libs/common/src/admin-console/services/organization/organization.service.spec.ts b/libs/common/src/admin-console/services/organization/organization.service.spec.ts deleted file mode 100644 index 6d2525966b..0000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.spec.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { firstValueFrom } from "rxjs"; - -import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { Utils } from "../../../platform/misc/utils"; -import { OrganizationId, UserId } from "../../../types/guid"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -import { OrganizationService, ORGANIZATIONS } from "./organization.service"; - -describe("OrganizationService", () => { - let organizationService: OrganizationService; - - const fakeUserId = Utils.newGuid() as UserId; - let fakeAccountService: FakeAccountService; - let fakeStateProvider: FakeStateProvider; - let fakeActiveUserState: FakeActiveUserState>; - - /** - * It is easier to read arrays than records in code, but we store a record - * in state. This helper methods lets us build organization arrays in tests - * and easily map them to records before storing them in state. - */ - function arrayToRecord(input: OrganizationData[]): Record { - if (input == null) { - return undefined; - } - return Object.fromEntries(input?.map((i) => [i.id, i])); - } - - /** - * There are a few assertions in this spec that check for array equality - * but want to ignore a specific index that _should_ be different. This - * function takes two arrays, and an index. It checks for equality of the - * arrays, but splices out the specified index from both arrays first. - */ - function expectIsEqualExceptForIndex(x: any[], y: any[], indexToExclude: number) { - // Clone the arrays to avoid modifying the reference values - const a = [...x]; - const b = [...y]; - delete a[indexToExclude]; - delete b[indexToExclude]; - expect(a).toEqual(b); - } - - /** - * Builds a simple mock `OrganizationData[]` array that can be used in tests - * to populate state. - * @param count The number of organizations to populate the list with. The - * function returns undefined if this is less than 1. The default value is 1. - * @param suffix A string to append to data fields on each organization. - * This defaults to the index of the organization in the list. - * @returns an `OrganizationData[]` array that can be used to populate - * stateProvider. - */ - function buildMockOrganizations(count = 1, suffix?: string): OrganizationData[] { - if (count < 1) { - return undefined; - } - - function buildMockOrganization(id: OrganizationId, name: string, identifier: string) { - const data = new OrganizationData({} as any, {} as any); - data.id = id; - data.name = name; - data.identifier = identifier; - - return data; - } - - const mockOrganizations = []; - for (let i = 0; i < count; i++) { - const s = suffix ? suffix + i.toString() : i.toString(); - mockOrganizations.push( - buildMockOrganization(("org" + s) as OrganizationId, "org" + s, "orgIdentifier" + s), - ); - } - - return mockOrganizations; - } - - /** - * `OrganizationService` deals with multiple accounts at times. This helper - * function can be used to add a new non-active account to the test data. - * This function is **not** needed to handle creation of the first account, - * as that is handled by the `FakeAccountService` in `mockAccountServiceWith()` - * @returns The `UserId` of the newly created state account and the mock data - * created for them as an `Organization[]`. - */ - async function addNonActiveAccountToStateProvider(): Promise<[UserId, OrganizationData[]]> { - const nonActiveUserId = Utils.newGuid() as UserId; - - const mockOrganizations = buildMockOrganizations(10); - const fakeNonActiveUserState = fakeStateProvider.singleUser.getFake( - nonActiveUserId, - ORGANIZATIONS, - ); - fakeNonActiveUserState.nextState(arrayToRecord(mockOrganizations)); - - return [nonActiveUserId, mockOrganizations]; - } - - beforeEach(async () => { - fakeAccountService = mockAccountServiceWith(fakeUserId); - fakeStateProvider = new FakeStateProvider(fakeAccountService); - fakeActiveUserState = fakeStateProvider.activeUser.getFake(ORGANIZATIONS); - organizationService = new OrganizationService(fakeStateProvider); - }); - - it("getAll", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const orgs = await organizationService.getAll(); - expect(orgs).toHaveLength(1); - const org = orgs[0]; - expect(org).toEqual(new Organization(mockData[0])); - }); - - describe("canManageSponsorships", () => { - it("can because one is available", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipAvailable = true; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can because one is used", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = "Something"; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(true); - }); - - it("can not because one isn't available or taken", async () => { - const mockData: OrganizationData[] = buildMockOrganizations(1); - mockData[0].familySponsorshipFriendlyName = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.canManageSponsorships$); - expect(result).toBe(false); - }); - }); - - describe("get", () => { - it("exists", async () => { - const mockData = buildMockOrganizations(1); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await organizationService.get(mockData[0].id); - expect(result).toEqual(new Organization(mockData[0])); - }); - - it("does not exist", async () => { - const result = await organizationService.get("this-org-does-not-exist"); - expect(result).toBe(undefined); - }); - }); - - describe("organizations$", () => { - describe("null checking behavior", () => { - it("publishes an empty array if organizations in state = undefined", async () => { - const mockData: OrganizationData[] = undefined; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = null", async () => { - const mockData: OrganizationData[] = null; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - - it("publishes an empty array if organizations in state = []", async () => { - const mockData: OrganizationData[] = []; - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - }); - }); - - describe("parameter handling & returns", () => { - it("publishes all organizations for the active user by default", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData); - }); - - it("can be used to publish the organizations of a non active user if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserState"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result).toEqual(nonActiveUserMockOrganizations); - expect(result).not.toEqual(await firstValueFrom(organizationService.organizations$)); - }); - }); - }); - - describe("upsert()", () => { - it("can create the organization list if necassary", async () => { - // Notice that no default state is provided in this test, so the list in - // `stateProvider` will be null when the `upsert` method is called. - const mockData = buildMockOrganizations(); - await organizationService.upsert(mockData[0]); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual(mockData.map((x) => new Organization(x))); - }); - - it("updates an organization that already exists in state, defaulting to the active user", async () => { - const mockData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(mockData)); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: mockData[indexToUpdate].id, - }; - await organizationService.upsert(anUpdatedOrganization); - const result = await firstValueFrom(organizationService.organizations$); - expect(result[indexToUpdate]).not.toEqual(new Organization(mockData[indexToUpdate])); - expect(result[indexToUpdate].id).toEqual(new Organization(mockData[indexToUpdate]).id); - expectIsEqualExceptForIndex( - result, - mockData.map((x) => new Organization(x)), - indexToUpdate, - ); - }); - - it("can also update an organization in state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, nonActiveUserMockOrganizations] = - await addNonActiveAccountToStateProvider(); - const indexToUpdate = 5; - const anUpdatedOrganization = { - ...buildMockOrganizations(1, "UPDATED").pop(), - id: nonActiveUserMockOrganizations[indexToUpdate].id, - }; - - await organizationService.upsert(anUpdatedOrganization, nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - - expect(result[indexToUpdate]).not.toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]), - ); - expect(result[indexToUpdate].id).toEqual( - new Organization(nonActiveUserMockOrganizations[indexToUpdate]).id, - ); - expectIsEqualExceptForIndex( - result, - nonActiveUserMockOrganizations.map((x) => new Organization(x)), - indexToUpdate, - ); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); - - describe("replace()", () => { - it("replaces the entire organization list in state, defaulting to the active user", async () => { - const originalData = buildMockOrganizations(10); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - - const newData = buildMockOrganizations(10, "newData"); - await organizationService.replace(arrayToRecord(newData)); - - const result = await firstValueFrom(organizationService.organizations$); - - expect(result).toEqual(newData); - expect(result).not.toEqual(originalData); - }); - - // This is more or less a test for logouts - it("can replace state with null", async () => { - const originalData = buildMockOrganizations(2); - fakeActiveUserState.nextState(arrayToRecord(originalData)); - await organizationService.replace(null); - const result = await firstValueFrom(organizationService.organizations$); - expect(result).toEqual([]); - expect(result).not.toEqual(originalData); - }); - - it("can also replace state for a non-active user, if requested", async () => { - const activeUserMockData = buildMockOrganizations(10, "activeUserOrganizations"); - fakeActiveUserState.nextState(arrayToRecord(activeUserMockData)); - - const [nonActiveUserId, originalOrganizations] = await addNonActiveAccountToStateProvider(); - const newData = buildMockOrganizations(10, "newData"); - - await organizationService.replace(arrayToRecord(newData), nonActiveUserId); - // This can be updated to use - // `firstValueFrom(organizations$(nonActiveUserId)` once all the - // promise based methods are removed from `OrganizationService` and the - // main observable is refactored to accept a userId - const result = await organizationService.getAll(nonActiveUserId); - expect(result).toEqual(newData); - expect(result).not.toEqual(originalOrganizations); - - // Just to be safe, lets make sure the active user didn't get updated - // at all - const activeUserState = await firstValueFrom(organizationService.organizations$); - expect(activeUserState).toEqual(activeUserMockData.map((x) => new Organization(x))); - expect(activeUserState).not.toEqual(result); - }); - }); -}); diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts deleted file mode 100644 index 49e906bdac..0000000000 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ /dev/null @@ -1,160 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { map, Observable, firstValueFrom } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { ORGANIZATIONS_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; -import { OrganizationData } from "../../models/data/organization.data"; -import { Organization } from "../../models/domain/organization"; - -/** - * The `KeyDefinition` for accessing organization lists in application state. - * @todo Ideally this wouldn't require a `fromJSON()` call, but `OrganizationData` - * has some properties that contain functions. This should probably get - * cleaned up. - */ -export const ORGANIZATIONS = UserKeyDefinition.record( - ORGANIZATIONS_DISK, - "organizations", - { - deserializer: (obj: Jsonify) => OrganizationData.fromJSON(obj), - clearOn: ["logout"], - }, -); - -/** - * Filter out organizations from an observable that __do not__ offer a - * families-for-enterprise sponsorship to members. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeOrganizationsWithoutFamilySponsorshipSupport() { - return map((orgs) => orgs.filter((o) => o.canManageSponsorships)); -} - -/** - * Filter out organizations from an observable that the organization user - * __is not__ a direct member of. This will exclude organizations only - * accessible as a provider. - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToExcludeProviderOrganizations() { - return map((orgs) => orgs.filter((o) => o.isMember)); -} - -/** - * Map an observable stream of organizations down to a boolean indicating - * if any organizations exist (`orgs.length > 0`). - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToBooleanHasAnyOrganizations() { - return map((orgs) => orgs.length > 0); -} - -/** - * Map an observable stream of organizations down to a single organization. - * @param `organizationId` The ID of the organization you'd like to subscribe to - * @returns a function that can be used in `Observable` pipes, - * like `organizationService.organizations$` - */ -function mapToSingleOrganization(organizationId: string) { - return map((orgs) => orgs?.find((o) => o.id === organizationId)); -} - -export class OrganizationService implements InternalOrganizationServiceAbstraction { - organizations$: Observable = this.getOrganizationsFromState$(); - memberOrganizations$: Observable = this.organizations$.pipe( - mapToExcludeProviderOrganizations(), - ); - - constructor(private stateProvider: StateProvider) {} - - get$(id: string): Observable { - return this.organizations$.pipe(mapToSingleOrganization(id)); - } - - getAll$(userId?: UserId): Observable { - return this.getOrganizationsFromState$(userId); - } - - async getAll(userId?: string): Promise { - return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); - } - - canManageSponsorships$ = this.organizations$.pipe( - mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), - mapToBooleanHasAnyOrganizations(), - ); - - familySponsorshipAvailable$ = this.organizations$.pipe( - map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)), - ); - - async hasOrganizations(): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations())); - } - - async upsert(organization: OrganizationData, userId?: UserId): Promise { - await this.stateFor(userId).update((existingOrganizations) => { - const organizations = existingOrganizations ?? {}; - organizations[organization.id] = organization; - return organizations; - }); - } - - async get(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - /** - * @deprecated For the CLI only - * @param id id of the organization - */ - async getFromState(id: string): Promise { - return await firstValueFrom(this.organizations$.pipe(mapToSingleOrganization(id))); - } - - async replace(organizations: { [id: string]: OrganizationData }, userId?: UserId): Promise { - await this.stateFor(userId).update(() => organizations); - } - - // Ideally this method would be renamed to organizations$() and the - // $organizations observable as it stands would be removed. This will - // require updates to callers, and so this method exists as a temporary - // workaround until we have time & a plan to update callers. - // - // It can be thought of as "organizations$ but with a userId option". - private getOrganizationsFromState$(userId?: UserId): Observable { - return this.stateFor(userId).state$.pipe(this.mapOrganizationRecordToArray()); - } - - /** - * Accepts a record of `OrganizationData`, which is how we store the - * organization list as a JSON object on disk, to an array of - * `Organization`, which is how the data is published to callers of the - * service. - * @returns a function that can be used to pipe organization data from - * stored state to an exposed object easily consumable by others. - */ - private mapOrganizationRecordToArray() { - return map, Organization[]>((orgs) => - Object.values(orgs ?? {})?.map((o) => new Organization(o)), - ); - } - - /** - * Fetches the organization list from on disk state for the specified user. - * @param userId the user ID to fetch the organization list for. Defaults to - * the currently active user. - * @returns an observable of organization state as it is stored on disk. - */ - private stateFor(userId?: UserId) { - return userId - ? this.stateProvider.getUser(userId, ORGANIZATIONS) - : this.stateProvider.getActive(ORGANIZATIONS); - } -} diff --git a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts b/libs/common/src/admin-console/services/organization/organization.state.ts similarity index 100% rename from libs/common/src/admin-console/services/organization/vnext-organization.state.ts rename to libs/common/src/admin-console/services/organization/organization.state.ts diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index d9802db9e3..f0ebfddf66 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -3,7 +3,6 @@ import { firstValueFrom, of } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; import { FakeActiveUserState } from "../../../../spec/fake-state"; -import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType, OrganizationUserType, @@ -18,6 +17,7 @@ import { Policy } from "../../../admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options"; import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service"; import { PolicyId, UserId } from "../../../types/guid"; +import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; describe("PolicyService", () => { const userId = "userId" as UserId; @@ -56,9 +56,7 @@ describe("PolicyService", () => { organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), ]); - organizationService.organizations$ = organizations$; - - organizationService.getAll$.mockReturnValue(organizations$); + organizationService.organizations$.mockReturnValue(organizations$); policyService = new PolicyService(stateProvider, organizationService); }); @@ -196,7 +194,7 @@ describe("PolicyService", () => { describe("getResetPasswordPolicyOptions", () => { it("default", async () => { - const result = policyService.getResetPasswordPolicyOptions(null, null); + const result = policyService.getResetPasswordPolicyOptions([], ""); expect(result).toEqual([new ResetPasswordPolicyOptions(), false]); }); diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index 7a04ba38aa..bf99b9ce72 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; import { PolicyId, UserId } from "../../../types/guid"; @@ -39,7 +39,11 @@ export class PolicyService implements InternalPolicyServiceAbstraction { map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.organizations$]).pipe( + const organizations$ = this.stateProvider.activeUserId$.pipe( + switchMap((userId) => this.organizationService.organizations$(userId)), + ); + + return combineLatest([filteredPolicies$, organizations$]).pipe( map( ([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)?.at(0) ?? null, @@ -53,7 +57,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction { map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.getAll$(userId)]).pipe( + return combineLatest([filteredPolicies$, this.organizationService.organizations$(userId)]).pipe( map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)), ); } diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 660f1124f4..843ac38301 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -1,11 +1,13 @@ import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { Organization } from "../../admin-console/models/domain/organization"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; @@ -95,7 +97,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(true, true, "https://other-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -110,7 +112,7 @@ describe("KeyConnectorService", () => { organizationData(true, false, "https://key-connector-url.com", 2, false), organizationData(false, false, "https://key-connector-url.com", 2, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -125,7 +127,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 0, false), organizationData(true, true, "https://key-connector-url.com", 1, false), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -140,7 +142,7 @@ describe("KeyConnectorService", () => { organizationData(true, true, "https://key-connector-url.com", 2, true), organizationData(false, true, "https://key-connector-url.com", 2, true), ]; - organizationService.getAll.mockResolvedValue(orgs); + organizationService.organizations$.mockReturnValue(of(orgs)); // Act const result = await keyConnectorService.getManagingOrganization(); @@ -181,7 +183,7 @@ describe("KeyConnectorService", () => { // create organization object const data = organizationData(true, true, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); // uses KeyConnector const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); @@ -195,7 +197,7 @@ describe("KeyConnectorService", () => { it("should return false if the user does not need migration", async () => { tokenService.getIsExternal.mockResolvedValue(false); const data = organizationData(false, false, "https://key-connector-url.com", 2, false); - organizationService.getAll.mockResolvedValue([data]); + organizationService.organizations$.mockReturnValue(of([data])); const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); state.nextState(true); @@ -275,7 +277,7 @@ describe("KeyConnectorService", () => { const masterKey = getMockMasterKey(); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); const error = new Error("Failed to post user key to key connector"); - organizationService.getAll.mockResolvedValue([organization]); + organizationService.organizations$.mockReturnValue(of([organization])); masterPasswordService.masterKeySubject.next(masterKey); jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f798413162..f6f76579ee 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -3,6 +3,8 @@ import { firstValueFrom } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { Argon2KdfConfig, KdfConfig, @@ -12,7 +14,6 @@ import { } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; import { Organization } from "../../admin-console/models/domain/organization"; import { KeysRequest } from "../../models/request/keys.request"; @@ -28,7 +29,6 @@ import { } from "../../platform/state"; import { UserId } from "../../types/guid"; import { MasterKey } from "../../types/key"; -import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; @@ -122,7 +122,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async getManagingOrganization(userId?: UserId): Promise { - const orgs = await this.organizationService.getAll(userId); + const orgs = await firstValueFrom(this.organizationService.organizations$(userId)); return orgs.find( (o) => o.keyConnectorEnabled && diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index cfa9030c9d..48f4b2b64f 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -8,6 +8,7 @@ import { ApiService } from "../../abstractions/api.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { getUserId } from "../../auth/services/account.service"; import { SyncCipherNotification, SyncFolderNotification, @@ -58,7 +59,7 @@ export abstract class CoreSyncService implements SyncService { abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; async getLastSync(): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (userId == null) { return null; } diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 138c7c0331..7acd7dd8c7 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { CollectionService, @@ -34,6 +34,7 @@ import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstraction import { TokenService } from "../../auth/abstractions/token.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; +import { getUserId } from "../../auth/services/account.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../billing/abstractions"; import { DomainsResponse } from "../../models/response/domains.response"; @@ -107,7 +108,7 @@ export class DefaultSyncService extends CoreSyncService { @sequentialize(() => "fullSync") override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.syncStarted(); const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); if (authStatus === AuthenticationStatus.LoggedOut) { diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index b06985e0ba..da38ca5bff 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, from, zip, Observable } from "rxjs"; +import { firstValueFrom, map, from, zip } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventType } from "../../enums"; @@ -17,8 +20,6 @@ import { CipherView } from "../../vault/models/view/cipher.view"; import { EVENT_COLLECTION } from "./key-definitions"; export class EventCollectionService implements EventCollectionServiceAbstraction { - private orgIds$: Observable; - constructor( private cipherService: CipherService, private stateProvider: StateProvider, @@ -26,11 +27,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private eventUploadService: EventUploadService, private authService: AuthService, private accountService: AccountService, - ) { - this.orgIds$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), - ); - } + ) {} + + private getOrgIds = (orgs: Organization[]): string[] => { + return orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []; + }; /** Adds an event to the active user's event collection * @param eventType the event type to be added @@ -42,14 +43,15 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ciphers: CipherView[], uploadImmediately = false, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(null, eventType, ciphers))) { return; } - const events$ = this.orgIds$.pipe( + const events$ = this.organizationService.organizations$(userId).pipe( + map((orgs) => this.getOrgIds(orgs)), map((orgs) => ciphers .filter((c) => orgs.includes(c.organizationId)) @@ -86,7 +88,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction uploadImmediately = false, organizationId: string = null, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) { @@ -122,8 +124,14 @@ export class EventCollectionService implements EventCollectionServiceAbstraction ): Promise { const cipher$ = from(this.cipherService.get(cipherId)); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const orgIds$ = this.organizationService + .organizations$(userId) + .pipe(map((orgs) => this.getOrgIds(orgs))); + const [authStatus, orgIds, cipher] = await firstValueFrom( - zip(this.authService.activeAccountStatus$, this.orgIds$, cipher$), + zip(this.authService.activeAccountStatus$, orgIds$, cipher$), ); // The user must be authorized diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index 15e8b03bfa..37ddfdeaee 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -1,11 +1,13 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { Observable, firstValueFrom, of } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "../../admin-console/models/domain/organization"; -import { CollectionId } from "../../types/guid"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; import { CipherView } from "../models/view/cipher.view"; import { @@ -18,6 +20,8 @@ describe("CipherAuthorizationService", () => { const mockCollectionService = mock(); const mockOrganizationService = mock(); + const mockUserId = Utils.newGuid() as UserId; + let mockAccountService: FakeAccountService; // Mock factories const createMockCipher = ( @@ -42,6 +46,7 @@ describe("CipherAuthorizationService", () => { isAdmin = false, editAnyCollection = false, } = {}) => ({ + id: "org1", allowAdminAccessToAllCollectionItems, canEditAllCiphers, canEditUnassignedCiphers, @@ -53,9 +58,11 @@ describe("CipherAuthorizationService", () => { beforeEach(() => { jest.clearAllMocks(); + mockAccountService = mockAccountServiceWith(mockUserId); cipherAuthorizationService = new DefaultCipherAuthorizationService( mockCollectionService, mockOrganizationService, + mockAccountService, ); }); @@ -72,7 +79,9 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); @@ -83,11 +92,13 @@ describe("CipherAuthorizationService", () => { it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => { const cipher = createMockCipher("org1", ["col1"]) as CipherView; const organization = createMockOrganization({ canEditAllCiphers: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization]) as Observable, + ); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(true); - expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1"); + expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId); done(); }); }); @@ -95,7 +106,7 @@ describe("CipherAuthorizationService", () => { it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ canEditUnassignedCiphers: false }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { expect(result).toBe(false); @@ -106,8 +117,8 @@ describe("CipherAuthorizationService", () => { it("should return true if activeCollectionId is provided and has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", true), @@ -132,8 +143,8 @@ describe("CipherAuthorizationService", () => { it("should return false if activeCollectionId is provided and manage permission is not present", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const activeCollectionId = "col1" as CollectionId; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -157,8 +168,8 @@ describe("CipherAuthorizationService", () => { it("should return true if any collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -182,8 +193,8 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", (done) => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; - const org = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + const organization = createMockOrganization(); + mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[])); const allCollections = [ createMockCollection("col1", false), @@ -216,7 +227,9 @@ describe("CipherAuthorizationService", () => { it("should return true for admin users", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ isAdmin: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -227,7 +240,9 @@ describe("CipherAuthorizationService", () => { it("should return true for custom user with canEditAnyCollection", async () => { const cipher = createMockCipher("org1", []) as CipherView; const organization = createMockOrganization({ editAnyCollection: true }); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const result = await firstValueFrom( cipherAuthorizationService.canCloneCipher$(cipher, true), @@ -240,7 +255,9 @@ describe("CipherAuthorizationService", () => { it("should return true if at least one cipher collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", true), @@ -257,7 +274,9 @@ describe("CipherAuthorizationService", () => { it("should return false if no collection has manage permission", async () => { const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; const organization = createMockOrganization(); - mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + mockOrganizationService.organizations$.mockReturnValue( + of([organization] as Organization[]), + ); const allCollections = [ createMockCollection("col1", false), diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 025d6b1cdc..fbee3ed862 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -3,9 +3,10 @@ import { map, Observable, of, shareReplay, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { CollectionId } from "@bitwarden/common/types/guid"; -import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { CollectionId } from "../../types/guid"; import { Cipher } from "../models/domain/cipher"; import { CipherView } from "../models/view/cipher.view"; @@ -51,8 +52,14 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer constructor( private collectionService: CollectionService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} + private organization$ = (cipher: CipherLike) => + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + map((orgs) => orgs.find((org) => org.id === cipher.organizationId)), + ); /** * * {@link CipherAuthorizationService.canDeleteCipher$} @@ -66,7 +73,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { if (isAdminConsoleAction) { // If the user is an admin, they can delete an unassigned cipher @@ -104,7 +111,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organizationService.get$(cipher.organizationId).pipe( + return this.organization$(cipher).pipe( switchMap((organization) => { // Admins and custom users can always clone when in the Admin Console if ( diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index f2bf7471d4..d574a6fc09 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -23,11 +23,15 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -138,8 +142,14 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { @Input() set organizationId(value: string) { this._organizationId = value; - this.organizationService - .get$(this._organizationId) + getUserId(this.accountService.activeAccount$) + .pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this._organizationId)), + ), + ) .pipe(takeUntil(this.destroy$)) .subscribe((organization) => { this._organizationId = organization?.id; @@ -294,8 +304,9 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { } private async initializeOrganizations() { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizations$ = concat( - this.organizationService.memberOrganizations$.pipe( + this.organizationService.memberOrganizations$(userId).pipe( // Import is an alternative way to create collections during onboarding, so import from Password Manager // is available to any user who can create collections in the organization. map((orgs) => orgs.filter((org) => org.canAccessImport || org.canCreateNewCollections)), @@ -408,7 +419,14 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { if (!organizationId) { return false; } - return (await this.organizationService.get(this.organizationId))?.canAccessImport; + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + return ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ) + )?.canAccessImport; } getFormatInstructionTitle() { diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts index 616f47ddb4..76b5a89384 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts @@ -5,8 +5,12 @@ import { Component, Input, OnInit } from "@angular/core"; 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, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CalloutModule } from "@bitwarden/components"; @Component({ @@ -42,7 +46,8 @@ export class ExportScopeCalloutComponent implements OnInit { ) {} async ngOnInit(): Promise { - if (!(await this.organizationService.hasOrganizations())) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (!(await firstValueFrom(this.organizationService.hasOrganizations(userId)))) { return; } @@ -51,12 +56,19 @@ export class ExportScopeCalloutComponent implements OnInit { } private async getScopeMessage(organizationId: string) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.scopeConfig = organizationId != null ? { title: "exportingOrganizationVaultTitle", description: "exportingOrganizationVaultDesc", - scopeIdentifier: (await this.organizationService.get(organizationId)).name, + scopeIdentifier: ( + await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(organizationId)), + ) + ).name, } : { title: "exportingPersonalVaultTitle", diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index a091b85265..934c35f806 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -12,17 +12,32 @@ import { ViewChild, } from "@angular/core"; import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms"; -import { combineLatest, map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; +import { + combineLatest, + firstValueFrom, + map, + merge, + Observable, + startWith, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + 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"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -81,8 +96,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { */ @Input() set organizationId(value: string) { this._organizationId = value; - this.organizationService - .get$(this._organizationId) + getUserId(this.accountService.activeAccount$) + .pipe( + switchMap((userId) => + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this._organizationId)), + ), + ) .pipe(takeUntil(this.destroy$)) .subscribe((organization) => { this._organizationId = organization?.id; @@ -168,6 +189,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { protected fileDownloadService: FileDownloadService, protected dialogService: DialogService, protected organizationService: OrganizationService, + private accountService: AccountService, private collectionService: CollectionService, ) {} @@ -194,10 +216,12 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { .pipe(startWith(0), takeUntil(this.destroy$)) .subscribe(() => this.adjustValidators()); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + if (this.organizationId) { - this.organizations$ = this.organizationService.memberOrganizations$.pipe( - map((orgs) => orgs.filter((org) => org.id == this.organizationId)), - ); + this.organizations$ = this.organizationService + .memberOrganizations$(userId) + .pipe(map((orgs) => orgs.filter((org) => org.id == this.organizationId))); this.exportForm.controls.vaultSelector.patchValue(this.organizationId); this.exportForm.controls.vaultSelector.disable(); @@ -207,7 +231,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.organizations$ = combineLatest({ collections: this.collectionService.decryptedCollections$, - memberOrganizations: this.organizationService.memberOrganizations$, + memberOrganizations: this.organizationService.memberOrganizations$(userId), }).pipe( map(({ collections, memberOrganizations }) => { const managedCollectionsOrgIds = new Set( diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 93a53345d3..28b13b51c6 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -8,7 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -46,7 +46,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { const [organizations, collections, allowPersonalOwnership, folders, cipher] = await firstValueFrom( combineLatest([ - this.organizations$, + this.organizations$(activeUserId), this.collectionService.encryptedCollections$.pipe( switchMap((c) => this.collectionService.decryptedCollections$.pipe( @@ -78,13 +78,17 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { }; } - private organizations$ = this.organizationService.organizations$.pipe( - map((orgs) => - orgs.filter( - (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, - ), - ), - ); + organizations$(userId: UserId) { + return this.organizationService + .organizations$(userId) + .pipe( + map((orgs) => + orgs.filter( + (o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed, + ), + ), + ); + } private allowPersonalOwnership$ = this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index f872ad0cf1..425afeace8 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -4,9 +4,13 @@ import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; import { CollectionId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -133,9 +137,12 @@ export class CipherViewComponent implements OnChanges, OnDestroy { ); } - if (this.cipher.organizationId) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + if (this.cipher.organizationId && userId) { this.organization$ = this.organizationService - .get$(this.cipher.organizationId) + .organizations$(userId) + .pipe(getOrganizationById(this.cipher.organizationId)) .pipe(takeUntil(this.destroyed$)); } diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 00852ff101..c950187edb 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -26,10 +26,14 @@ import { import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -128,28 +132,31 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI protected orgName: string; protected showOrgSelector: boolean = false; - protected organizations$: Observable = - this.organizationService.organizations$.pipe( - map((orgs) => - orgs - .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) - .sort((a, b) => a.name.localeCompare(b.name)), - ), - tap((orgs) => { - if (orgs.length > 0 && this.showOrgSelector) { - // Using setTimeout to defer the patchValue call until the next event loop cycle - setTimeout(() => { - this.formGroup.patchValue({ selectedOrg: orgs[0].id }); - this.setFormValidators(); + protected organizations$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.organizationService.organizations$(account?.id).pipe( + map((orgs) => + orgs + .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) + .sort((a, b) => a.name.localeCompare(b.name)), + ), + tap((orgs) => { + if (orgs.length > 0 && this.showOrgSelector) { + // Using setTimeout to defer the patchValue call until the next event loop cycle + setTimeout(() => { + this.formGroup.patchValue({ selectedOrg: orgs[0].id }); + this.setFormValidators(); - // Disable the org selector if there is only one organization - if (orgs.length === 1) { - this.formGroup.controls.selectedOrg.disable(); - } - }); - } - }), - ); + // Disable the org selector if there is only one organization + if (orgs.length === 1) { + this.formGroup.controls.selectedOrg.disable(); + } + }); + } + }), + ), + ), + ); protected transferWarningText = (orgName: string, itemsCount: number) => { const haveOrgName = !!orgName; @@ -354,7 +361,10 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI return; } - const org = await this.organizationService.get(organizationId); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const org = await firstValueFrom( + this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)), + ); this.orgName = org.name; this.editableItems = org.canEditAllCiphers @@ -408,7 +418,9 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI private getCollectionsForOrganization(orgId: OrganizationId): Observable { return combineLatest([ this.collectionService.decryptedCollections$, - this.organizationService.organizations$, + this.accountService.activeAccount$.pipe( + switchMap((account) => this.organizationService.organizations$(account?.id)), + ), ]).pipe( map(([collections, organizations]) => { const org = organizations.find((o) => o.id === orgId); diff --git a/libs/vault/src/components/password-reprompt.component.ts b/libs/vault/src/components/password-reprompt.component.ts index 6898d41171..dc75d50c93 100644 --- a/libs/vault/src/components/password-reprompt.component.ts +++ b/libs/vault/src/components/password-reprompt.component.ts @@ -1,10 +1,11 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Component } from "@angular/core"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { @@ -57,7 +58,7 @@ export class PasswordRepromptComponent { return; } - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (userId == null) { throw new Error("An active user is expected while doing password reprompt."); diff --git a/libs/vault/src/tasks/services/default-task.service.spec.ts b/libs/vault/src/tasks/services/default-task.service.spec.ts index 850d0bcc2b..26b1a79ca2 100644 --- a/libs/vault/src/tasks/services/default-task.service.spec.ts +++ b/libs/vault/src/tasks/services/default-task.service.spec.ts @@ -43,7 +43,7 @@ describe("Default task service", () => { { provide: OrganizationService, useValue: { - getAll$: mockGetAllOrgs$, + organizations$: mockGetAllOrgs$, }, }, ], diff --git a/libs/vault/src/tasks/services/default-task.service.ts b/libs/vault/src/tasks/services/default-task.service.ts index 2fc4ba3a93..f5c1d95af0 100644 --- a/libs/vault/src/tasks/services/default-task.service.ts +++ b/libs/vault/src/tasks/services/default-task.service.ts @@ -23,7 +23,7 @@ export class DefaultTaskService implements TaskService { tasksEnabled$ = perUserCache$((userId) => { return this.organizationService - .getAll$(userId) + .organizations$(userId) .pipe(map((orgs) => orgs.some((o) => o.useRiskInsights))); }); diff --git a/package-lock.json b/package-lock.json index 3e87e3f469..22565c93c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10341,6 +10341,7 @@ "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/types": "8.20.0", "@typescript-eslint/visitor-keys": "8.20.0", @@ -10368,6 +10369,7 @@ "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.20.0", @@ -10392,6 +10394,7 @@ "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/types": "8.20.0", "eslint-visitor-keys": "^4.2.0" @@ -30578,6 +30581,7 @@ "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18.12" },