diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index 60b38570482..abe642970bb 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -99,7 +99,7 @@ describe("AccountSecurityComponent", () => { it("pin enabled when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); @@ -111,7 +111,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -129,7 +129,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -143,7 +143,7 @@ describe("AccountSecurityComponent", () => { it("pin visible when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); fixture.detectChanges(); @@ -158,7 +158,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); @@ -173,7 +173,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); pinServiceAbstraction.isPinSet.mockResolvedValue(true); @@ -190,7 +190,7 @@ describe("AccountSecurityComponent", () => { policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 8cdfdab9524..75b59b8efdc 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -27,8 +27,10 @@ import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitward import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; 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 { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeout, @@ -152,8 +154,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); this.showMasterPasswordOnClientRestartOption = hasMasterPassword; - const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout); - if ((await firstValueFrom(this.policyService.get$(PolicyType.MaximumVaultTimeout))) != null) { + const maximumVaultTimeoutPolicy = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId), + ), + getFirstPolicy, + ); + if ((await firstValueFrom(maximumVaultTimeoutPolicy)) != null) { this.hasVaultTimeoutPolicy = true; } @@ -195,7 +203,12 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { timeout = VaultTimeoutStringType.OnRestart; } - this.pinEnabled$ = this.policyService.get$(PolicyType.RemoveUnlockWithPin).pipe( + this.pinEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.RemoveUnlockWithPin, userId), + ), + getFirstPolicy, map((policy) => { return policy == null || !policy.enabled; }), diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts index a300ac08660..9f197b02193 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts @@ -8,6 +8,9 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; @@ -35,10 +38,12 @@ describe("AutoSubmitLoginBackground", () => { let configService: MockProxy; let platformUtilsService: MockProxy; let policyDetails: MockProxy; - let automaticAppLogInPolicy$: BehaviorSubject; - let policyAppliesToActiveUser$: BehaviorSubject; + let automaticAppLogInPolicy$: BehaviorSubject; + let policyAppliesToUser$: BehaviorSubject; let policyService: MockProxy; let autoSubmitLoginBackground: AutoSubmitLoginBackground; + let accountService: FakeAccountService; + const mockUserId = Utils.newGuid() as UserId; const validIpdUrl1 = "https://example.com"; const validIpdUrl2 = "https://subdomain.example3.com"; const validAutoSubmitHost = "some-valid-url.com"; @@ -61,12 +66,13 @@ describe("AutoSubmitLoginBackground", () => { idpHost: `${validIpdUrl1} , https://example2.com/some/sub-route ,${validIpdUrl2}, [invalidValue] ,,`, }, }); - automaticAppLogInPolicy$ = new BehaviorSubject(policyDetails); - policyAppliesToActiveUser$ = new BehaviorSubject(true); + automaticAppLogInPolicy$ = new BehaviorSubject([policyDetails]); + policyAppliesToUser$ = new BehaviorSubject(true); policyService = mock({ - get$: jest.fn().mockReturnValue(automaticAppLogInPolicy$), - policyAppliesToActiveUser$: jest.fn().mockReturnValue(policyAppliesToActiveUser$), + policiesByType$: jest.fn().mockReturnValue(automaticAppLogInPolicy$), + policyAppliesToUser$: jest.fn().mockReturnValue(policyAppliesToUser$), }); + accountService = mockAccountServiceWith(mockUserId); autoSubmitLoginBackground = new AutoSubmitLoginBackground( logService, autofillService, @@ -75,6 +81,7 @@ describe("AutoSubmitLoginBackground", () => { configService, platformUtilsService, policyService, + accountService, ); }); @@ -84,7 +91,7 @@ describe("AutoSubmitLoginBackground", () => { describe("when the AutoSubmitLoginBackground feature is disabled", () => { it("destroys all event listeners when the AutomaticAppLogIn policy is not enabled", async () => { - automaticAppLogInPolicy$.next(mock({ ...policyDetails, enabled: false })); + automaticAppLogInPolicy$.next([mock({ ...policyDetails, enabled: false })]); await autoSubmitLoginBackground.init(); @@ -92,7 +99,7 @@ describe("AutoSubmitLoginBackground", () => { }); it("destroys all event listeners when the AutomaticAppLogIn policy does not apply to the current user", async () => { - policyAppliesToActiveUser$.next(false); + policyAppliesToUser$.next(false); await autoSubmitLoginBackground.init(); @@ -100,7 +107,7 @@ describe("AutoSubmitLoginBackground", () => { }); it("destroys all event listeners when the idpHost is not specified in the AutomaticAppLogIn policy", async () => { - automaticAppLogInPolicy$.next(mock({ ...policyDetails, data: { idpHost: "" } })); + automaticAppLogInPolicy$.next([mock({ ...policyDetails, data: { idpHost: "" } })]); await autoSubmitLoginBackground.init(); @@ -264,6 +271,7 @@ describe("AutoSubmitLoginBackground", () => { configService, platformUtilsService, policyService, + accountService, ); jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(tab); }); diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.ts b/apps/browser/src/autofill/background/auto-submit-login.background.ts index 034938ca521..bce876e8f82 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.ts @@ -1,12 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -42,6 +45,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr private configService: ConfigService, private platformUtilsService: PlatformUtilsService, private policyService: PolicyService, + private accountService: AccountService, ) { this.isSafariBrowser = this.platformUtilsService.isSafari(); } @@ -56,8 +60,14 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr FeatureFlag.IdpAutoSubmitLogin, ); if (featureFlagEnabled) { - this.policyService - .get$(PolicyType.AutomaticAppLogIn) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.AutomaticAppLogIn, userId), + ), + getFirstPolicy, + ) .subscribe(this.handleAutoSubmitLoginPolicySubscription.bind(this)); } } @@ -86,7 +96,12 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr */ private applyPolicyToActiveUser = async (policy: Policy) => { const policyAppliesToUser = await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.AutomaticAppLogIn), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.AutomaticAppLogIn, userId), + ), + ), ); if (!policyAppliesToUser) { diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index d474e303336..ebdd244e140 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -2,7 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; @@ -51,7 +51,7 @@ describe("NotificationBackground", () => { const cipherService = mock(); let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; - const policyService = mock(); + const policyService = mock(); const folderService = mock(); const userNotificationSettingsService = mock(); const domainSettingsService = mock(); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 50e0ee0aa75..c2e90460dfc 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, 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"; @@ -8,7 +8,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; +import { getOptionalUserId, getUserId } from "@bitwarden/common/auth/services/account.service"; import { ExtensionCommand, ExtensionCommandType, @@ -743,7 +743,12 @@ export default class NotificationBackground { private async removeIndividualVault(): Promise { return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + ), ); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a396123f830..e1f0b8bfc64 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -26,8 +26,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { DefaultOrganizationService } from "@bitwarden/common/admin-console/services/organization/default-organization.service"; +import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.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"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -685,7 +685,7 @@ export default class MainBackground { this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.organizationService = new DefaultOrganizationService(this.stateProvider); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); + this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new DefaultVaultTimeoutSettingsService( this.accountService, @@ -728,9 +728,14 @@ export default class MainBackground { this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, this.policyService, + this.accountService, ); this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); + this.policyApiService = new PolicyApiService( + this.policyService, + this.apiService, + this.accountService, + ); this.keyConnectorService = new KeyConnectorService( this.accountService, this.masterPasswordService, @@ -1202,6 +1207,7 @@ export default class MainBackground { this.configService, this.platformUtilsService, this.policyService, + this.accountService, ); const contextMenuClickedHandler = new ContextMenuClickedHandler( diff --git a/apps/browser/src/billing/services/families-policy.service.spec.ts b/apps/browser/src/billing/services/families-policy.service.spec.ts index 65a861038bf..e9f75d52cb6 100644 --- a/apps/browser/src/billing/services/families-policy.service.spec.ts +++ b/apps/browser/src/billing/services/families-policy.service.spec.ts @@ -51,7 +51,7 @@ describe("FamiliesPolicyService", () => { organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: true }] as Policy[]; - policyService.getAll$.mockReturnValue(of(policies)); + policyService.policiesByType$.mockReturnValue(of(policies)); const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$()); expect(result).toBe(true); @@ -64,7 +64,7 @@ describe("FamiliesPolicyService", () => { organizationService.organizations$.mockReturnValue(of(organizations)); const policies = [{ organizationId: "org1", enabled: false }] as Policy[]; - policyService.getAll$.mockReturnValue(of(policies)); + policyService.policiesByType$.mockReturnValue(of(policies)); const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$()); expect(result).toBe(false); diff --git a/apps/browser/src/billing/services/families-policy.service.ts b/apps/browser/src/billing/services/families-policy.service.ts index 755d3e84591..42fa43cab1d 100644 --- a/apps/browser/src/billing/services/families-policy.service.ts +++ b/apps/browser/src/billing/services/families-policy.service.ts @@ -47,7 +47,7 @@ export class FamiliesPolicyService { map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id), switchMap((enterpriseOrgId) => this.policyService - .getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId) + .policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId) .pipe( map( (policies) => diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 86ab11a374a..42a05f14007 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -482,7 +482,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: AutofillSettingsServiceAbstraction, useClass: AutofillSettingsService, - deps: [StateProvider, PolicyService], + deps: [StateProvider, PolicyService, AccountService], }), safeProvider({ provide: UserNotificationSettingsServiceAbstraction, diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index c3f4634a6c2..6fc4793f5c0 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -64,7 +64,7 @@ describe("SendV2Component", () => { }); policyService = mock(); - policyService.policyAppliesToActiveUser$.mockReturnValue(of(true)); // Return `true` by default + policyService.policyAppliesToUser$.mockReturnValue(of(true)); // Return `true` by default sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts index 2766ba56c95..49804abda5d 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -2,11 +2,13 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { RouterLink } from "@angular/router"; -import { combineLatest } from "rxjs"; +import { combineLatest, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; 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"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components"; import { @@ -66,6 +68,7 @@ export class SendV2Component implements OnInit, OnDestroy { protected sendItemsService: SendItemsService, protected sendListFiltersService: SendListFiltersService, private policyService: PolicyService, + private accountService: AccountService, ) { combineLatest([ this.sendItemsService.emptyList$, @@ -93,9 +96,14 @@ export class SendV2Component implements OnInit, OnDestroy { this.listState = null; }); - this.policyService - .policyAppliesToActiveUser$(PolicyType.DisableSend) - .pipe(takeUntilDestroyed()) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId), + ), + takeUntilDestroyed(), + ) .subscribe((sendsDisabled) => { this.sendsDisabled = sendsDisabled; }); 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 99a27c54bcc..f9785bccd00 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 @@ -36,7 +36,7 @@ describe("VaultPopupListFiltersService", () => { let folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); let decryptedCollections$ = new BehaviorSubject([]); - const policyAppliesToActiveUser$ = new BehaviorSubject(false); + const policyAppliesToUser$ = new BehaviorSubject(false); let viewCacheService: { signal: jest.Mock; mockSignal: WritableSignal; @@ -65,7 +65,7 @@ describe("VaultPopupListFiltersService", () => { } as I18nService; const policyService = { - policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$), + policyAppliesToUser$: jest.fn(() => policyAppliesToUser$), }; const state$ = new BehaviorSubject(false); @@ -75,8 +75,8 @@ describe("VaultPopupListFiltersService", () => { _memberOrganizations$ = new BehaviorSubject([]); // Fresh instance per test folderViews$ = new BehaviorSubject([]); // Fresh instance per test decryptedCollections$ = new BehaviorSubject([]); // Fresh instance per test - policyAppliesToActiveUser$.next(false); - policyService.policyAppliesToActiveUser$.mockClear(); + policyAppliesToUser$.next(false); + policyService.policyAppliesToUser$.mockClear(); const accountService = mockAccountServiceWith("userId" as UserId); const mockCachedSignal = createMockSignal({}); @@ -196,14 +196,15 @@ describe("VaultPopupListFiltersService", () => { }); describe("PersonalOwnership policy", () => { - it('calls policyAppliesToActiveUser$ with "PersonalOwnership"', () => { - expect(policyService.policyAppliesToActiveUser$).toHaveBeenCalledWith( + it('calls policyAppliesToUser$ with "PersonalOwnership"', () => { + expect(policyService.policyAppliesToUser$).toHaveBeenCalledWith( PolicyType.PersonalOwnership, + "userId", ); }); it("returns an empty array when the policy applies and there is a single organization", (done) => { - policyAppliesToActiveUser$.next(true); + policyAppliesToUser$.next(true); _memberOrganizations$.next([ { name: "bobby's org", id: "1234-3323-23223" }, ] as Organization[]); @@ -215,7 +216,7 @@ describe("VaultPopupListFiltersService", () => { }); it('adds "myVault" when the policy does not apply and there are multiple organizations', (done) => { - policyAppliesToActiveUser$.next(false); + policyAppliesToUser$.next(false); const orgs = [ { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-4343-99888" }, @@ -234,7 +235,7 @@ describe("VaultPopupListFiltersService", () => { }); it('does not add "myVault" the policy applies and there are multiple organizations', (done) => { - policyAppliesToActiveUser$.next(true); + policyAppliesToUser$.next(true); const orgs = [ { name: "bobby's org", id: "1234-3323-23223" }, { name: "alice's org", id: "2223-3242-99888" }, @@ -679,7 +680,7 @@ function createSeededVaultPopupListFiltersService( } as any; const policyServiceMock = { - policyAppliesToActiveUser$: jest.fn(() => new BehaviorSubject(false)), + policyAppliesToUser$: jest.fn(() => new BehaviorSubject(false)), } as any; const stateProviderMock = { 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 8d9f6664e45..187c8772e88 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 @@ -8,7 +8,6 @@ import { filter, map, Observable, - of, shareReplay, startWith, switchMap, @@ -23,6 +22,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 { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -288,68 +288,70 @@ export class VaultPopupListFiltersService { /** * Organization array structured to be directly passed to `ChipSelectComponent` */ - organizations$: Observable[]> = combineLatest([ + + organizations$: Observable[]> = this.accountService.activeAccount$.pipe( - switchMap((account) => - account === null ? of([]) : this.organizationService.memberOrganizations$(account.id), + getUserId, + switchMap((userId) => + combineLatest([ + this.organizationService.memberOrganizations$(userId), + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ]), ), - ), - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), - ]).pipe( - map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ - orgs.sort(Utils.getSortFunction(this.i18nService, "name")), - personalOwnershipApplies, - ]), - map(([orgs, personalOwnershipApplies]) => { - // When there are no organizations return an empty array, - // resulting in the org filter being hidden - if (!orgs.length) { - return []; - } + map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ + orgs.sort(Utils.getSortFunction(this.i18nService, "name")), + personalOwnershipApplies, + ]), + map(([orgs, personalOwnershipApplies]) => { + // When there are no organizations return an empty array, + // resulting in the org filter being hidden + if (!orgs.length) { + return []; + } - // When there is only one organization and personal ownership policy applies, - // return an empty array, resulting in the org filter being hidden - if (orgs.length === 1 && personalOwnershipApplies) { - return []; - } + // When there is only one organization and personal ownership policy applies, + // return an empty array, resulting in the org filter being hidden + if (orgs.length === 1 && personalOwnershipApplies) { + return []; + } - const myVaultOrg: ChipSelectOption[] = []; + const myVaultOrg: ChipSelectOption[] = []; - // Only add "My vault" if personal ownership policy does not apply - if (!personalOwnershipApplies) { - myVaultOrg.push({ - value: { id: MY_VAULT_ID } as Organization, - label: this.i18nService.t("myVault"), - icon: "bwi-user", - }); - } + // Only add "My vault" if personal ownership policy does not apply + if (!personalOwnershipApplies) { + myVaultOrg.push({ + value: { id: MY_VAULT_ID } as Organization, + label: this.i18nService.t("myVault"), + icon: "bwi-user", + }); + } - return [ - ...myVaultOrg, - ...orgs.map((org) => { - let icon = "bwi-business"; + return [ + ...myVaultOrg, + ...orgs.map((org) => { + let icon = "bwi-business"; - if (!org.enabled) { - // Show a warning icon if the organization is deactivated - icon = "bwi-exclamation-triangle tw-text-danger"; - } else if ( - org.productTierType === ProductTierType.Families || - org.productTierType === ProductTierType.Free - ) { - // Show a family icon if the organization is a family or free org - icon = "bwi-family"; - } + if (!org.enabled) { + // Show a warning icon if the organization is deactivated + icon = "bwi-exclamation-triangle tw-text-danger"; + } else if ( + org.productTierType === ProductTierType.Families || + org.productTierType === ProductTierType.Free + ) { + // Show a family icon if the organization is a family or free org + icon = "bwi-family"; + } - return { - value: org, - label: org.name, - icon, - }; - }), - ]; - }), - shareReplay({ refCount: true, bufferSize: 1 }), - ); + return { + value: org, + label: org.name, + icon, + }; + }), + ]; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); /** * Folder array structured to be directly passed to `ChipSelectComponent` diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 9af6e1f0613..107afc6dc8d 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -5,7 +5,7 @@ import * as http from "http"; import { OptionValues } from "commander"; import * as inquirer from "inquirer"; import Separator from "inquirer/lib/objects/separator"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { LoginStrategyServiceAbstraction, @@ -29,6 +29,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -555,7 +556,10 @@ export class LoginCommand { ); const enforcedPolicyOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + ), ); // Verify master password meets policy requirements diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index c82a744b4a1..5bc07f63c32 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -28,8 +28,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.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"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -237,7 +237,7 @@ export class ServiceContainer { cryptoFunctionService: NodeCryptoFunctionService; encryptService: EncryptServiceImplementation; authService: AuthService; - policyService: PolicyService; + policyService: DefaultPolicyService; policyApiService: PolicyApiServiceAbstraction; logService: ConsoleLogService; sendService: SendService; @@ -469,7 +469,7 @@ export class ServiceContainer { this.ssoUrlService = new SsoUrlService(); this.organizationService = new DefaultOrganizationService(this.stateProvider); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); + this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService); this.vaultTimeoutSettingsService = new DefaultVaultTimeoutSettingsService( this.accountService, @@ -560,7 +560,11 @@ export class ServiceContainer { this.providerService = new ProviderService(this.stateProvider); - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); + this.policyApiService = new PolicyApiService( + this.policyService, + this.apiService, + this.accountService, + ); this.keyConnectorService = new KeyConnectorService( this.accountService, @@ -672,6 +676,7 @@ export class ServiceContainer { this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, this.policyService, + this.accountService, ); this.cipherService = new CipherService( diff --git a/apps/cli/src/tools/export.command.ts b/apps/cli/src/tools/export.command.ts index df6e7bae1cb..f5fea794eef 100644 --- a/apps/cli/src/tools/export.command.ts +++ b/apps/cli/src/tools/export.command.ts @@ -2,10 +2,13 @@ // @ts-strict-ignore import { OptionValues } from "commander"; import * as inquirer from "inquirer"; +import { firstValueFrom, switchMap } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; 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"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -26,14 +29,19 @@ export class ExportCommand { private exportService: VaultExportServiceAbstraction, private policyService: PolicyService, private eventCollectionService: EventCollectionService, + private accountService: AccountService, private configService: ConfigService, ) {} async run(options: OptionValues): Promise { - if ( - options.organizationid == null && - (await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport)) - ) { + const policyApplies$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisablePersonalVaultExport, userId), + ), + ); + + if (options.organizationid == null && (await firstValueFrom(policyApplies$))) { return Response.badRequest( "One or more organization policies prevents you from exporting your personal vault.", ); diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 8b9d441f0ff..81816540d12 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -501,6 +501,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.exportService, this.serviceContainer.policyService, this.serviceContainer.eventCollectionService, + this.serviceContainer.accountService, this.serviceContainer.configService, ); const response = await command.run(options); diff --git a/apps/desktop/src/app/accounts/settings.component.spec.ts b/apps/desktop/src/app/accounts/settings.component.spec.ts index 342d4717511..d29147c1823 100644 --- a/apps/desktop/src/app/accounts/settings.component.spec.ts +++ b/apps/desktop/src/app/accounts/settings.component.spec.ts @@ -153,7 +153,7 @@ describe("SettingsComponent", () => { it("pin enabled when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); @@ -164,7 +164,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -175,7 +175,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); @@ -184,7 +184,7 @@ describe("SettingsComponent", () => { it("pin visible when RemoveUnlockWithPin policy is not set", async () => { // @ts-strict-ignore - policyService.get$.mockReturnValue(of(null)); + policyService.policiesByType$.mockReturnValue(of([null])); await component.ngOnInit(); fixture.detectChanges(); @@ -201,7 +201,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); @@ -218,7 +218,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); pinServiceAbstraction.isPinSet.mockResolvedValue(true); await component.ngOnInit(); @@ -236,7 +236,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); await component.ngOnInit(); fixture.detectChanges(); @@ -255,7 +255,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = false; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); i18nService.t.mockImplementation((id: string) => { if (id === "requirePasswordOnStart") { @@ -290,7 +290,7 @@ describe("SettingsComponent", () => { const policy = new Policy(); policy.type = PolicyType.RemoveUnlockWithPin; policy.enabled = true; - policyService.get$.mockReturnValue(of(policy)); + policyService.policiesByType$.mockReturnValue(of([policy])); platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); i18nService.t.mockImplementation((id: string) => { if (id === "requirePasswordOnStart") { diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index cec0d98ccac..20b6d509f4d 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -17,8 +17,10 @@ import { import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DeviceType } from "@bitwarden/common/enums"; @@ -235,7 +237,12 @@ export class SettingsComponent implements OnInit, OnDestroy { ); // Load timeout policy - this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe( + this.vaultTimeoutPolicyCallout = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId), + ), + getFirstPolicy, filter((policy) => policy != null), map((policy) => { let timeout; @@ -259,7 +266,12 @@ export class SettingsComponent implements OnInit, OnDestroy { // Load initial values this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id); - this.pinEnabled$ = this.policyService.get$(PolicyType.RemoveUnlockWithPin).pipe( + this.pinEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.RemoveUnlockWithPin, userId), + ), + getFirstPolicy, map((policy) => { return policy == null || !policy.enabled; }), diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index a8a1e738644..591ff6fa8cf 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.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 } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -144,10 +144,14 @@ export class EncryptedMessageHandlerService { const credentialCreatePayload = payload as CredentialCreatePayload; - if ( - credentialCreatePayload.name == null || - (await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership)) - ) { + const policyApplies$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + ); + + if (credentialCreatePayload.name == null || (await firstValueFrom(policyApplies$))) { return { status: "failure" }; } diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 73973e7ffde..43d8f910d0f 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -5,6 +5,7 @@ import { firstValueFrom, Subject } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.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/billing-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -48,6 +49,7 @@ export class VaultFilterComponent protected billingApiService: BillingApiServiceAbstraction, protected dialogService: DialogService, protected configService: ConfigService, + protected accountService: AccountService, ) { super( vaultFilterService, @@ -58,6 +60,7 @@ export class VaultFilterComponent billingApiService, dialogService, configService, + accountService, ); } 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 0f3ca55da6f..daa1077a0f3 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 @@ -118,7 +118,10 @@ export class OrganizationLayoutComponent implements OnInit { ), ); - this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg); + this.hideNewOrgButton$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)), + ); const provider$ = this.organization$.pipe( switchMap((organization) => this.providerService.get$(organization.providerId)), diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 1b08d081823..84cb541e9f4 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -3,11 +3,13 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, switchMap, takeUntil } from "rxjs"; import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -81,12 +83,16 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { private toastService: ToastService, private formBuilder: FormBuilder, private dialogRef: DialogRef, + private accountService: AccountService, ) {} async ngOnInit() { - this.policyService - .masterPasswordPolicyOptions$() - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + takeUntil(this.destroy$), + ) .subscribe( (enforcedPasswordPolicyOptions) => (this.enforcedPolicyOptions = enforcedPasswordPolicyOptions), 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 0bfdde8fc97..a64247339a5 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 @@ -43,6 +43,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga 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 { getUserId } from "@bitwarden/common/auth/services/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"; @@ -168,15 +169,18 @@ export class MembersComponent extends BaseMembersComponent this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager)); - const policies$ = organization$.pipe( - switchMap((organization) => { + const policies$ = combineLatest([ + this.accountService.activeAccount$.pipe(getUserId), + organization$, + ]).pipe( + switchMap(([userId, organization]) => { if (organization.isProviderUser) { return from(this.policyApiService.getPolicies(organization.id)).pipe( map((response) => Policy.fromListResponse(response)), ); } - return this.policyService.policies$; + return this.policyService.policies$(userId); }), ); diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts index 5d5770e2325..57ea5e8ee05 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts @@ -8,11 +8,15 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; // FIXME: remove `src` and fix import @@ -38,6 +42,8 @@ describe("WebLoginComponentService", () => { let passwordGenerationService: MockProxy; let platformUtilsService: MockProxy; let ssoLoginService: MockProxy; + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; beforeEach(() => { acceptOrganizationInviteService = mock(); @@ -50,6 +56,7 @@ describe("WebLoginComponentService", () => { passwordGenerationService = mock(); platformUtilsService = mock(); ssoLoginService = mock(); + accountService = mockAccountServiceWith(mockUserId); TestBed.configureTestingModule({ providers: [ @@ -65,6 +72,7 @@ describe("WebLoginComponentService", () => { { provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService }, { provide: PlatformUtilsService, useValue: platformUtilsService }, { provide: SsoLoginServiceAbstraction, useValue: ssoLoginService }, + { provide: AccountService, useValue: accountService }, ], }); service = TestBed.inject(WebLoginComponentService); diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts index 29f2f237ec1..8d4c3bd84f0 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; import { DefaultLoginComponentService, @@ -12,7 +12,9 @@ import { import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -39,6 +41,7 @@ export class WebLoginComponentService platformUtilsService: PlatformUtilsService, ssoLoginService: SsoLoginServiceAbstraction, private router: Router, + private accountService: AccountService, ) { super( cryptoFunctionService, @@ -93,7 +96,10 @@ export class WebLoginComponentService resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled; const enforcedPasswordPolicyOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(policies), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + ), ); return { diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 7409fa3bea3..48b74dc5e2e 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -10,9 +10,12 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; @@ -30,6 +33,8 @@ describe("WebRegistrationFinishService", () => { let policyApiService: MockProxy; let logService: MockProxy; let policyService: MockProxy; + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; beforeEach(() => { keyService = mock(); @@ -38,6 +43,7 @@ describe("WebRegistrationFinishService", () => { policyApiService = mock(); logService = mock(); policyService = mock(); + accountService = mockAccountServiceWith(mockUserId); service = new WebRegistrationFinishService( keyService, @@ -46,6 +52,7 @@ describe("WebRegistrationFinishService", () => { policyApiService, logService, policyService, + accountService, ); }); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index 569b417a3cb..3d99b3b6712 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -30,6 +31,7 @@ export class WebRegistrationFinishService private policyApiService: PolicyApiServiceAbstraction, private logService: LogService, private policyService: PolicyService, + private accountService: AccountService, ) { super(keyService, accountApiService); } @@ -68,7 +70,7 @@ export class WebRegistrationFinishService } const masterPasswordPolicyOpts: MasterPasswordPolicyOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(policies), + this.policyService.masterPasswordPolicyOptions$(null, policies), ); return masterPasswordPolicyOpts; diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index 68f439d34a4..6eee68585db 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -3,11 +3,12 @@ import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, OnDestroy, OnInit, Inject, Input } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { takeUntil } from "rxjs"; +import { switchMap, takeUntil } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -79,9 +80,12 @@ export class EmergencyAccessTakeoverComponent const policies = await this.emergencyAccessService.getGrantorPolicies( this.params.emergencyAccessId, ); - this.policyService - .masterPasswordPolicyOptions$(policies) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)), + takeUntil(this.destroy$), + ) .subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions)); } 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 83c9ff23f3c..582a9412182 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 @@ -25,6 +25,7 @@ import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response"; import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -109,13 +110,17 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.providers.sort((a: any, b: any) => a.sort - b.sort); - this.policyService - .policyAppliesToActiveUser$(PolicyType.TwoFactorAuthentication) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.TwoFactorAuthentication, userId), + ), + takeUntil(this.destroy$), + ) .subscribe((policyAppliesToActiveUser) => { this.twoFactorAuthPolicyAppliesToActiveUser = policyAppliesToActiveUser; }); - await this.load(); } diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts index c84800aefd4..13c7993768c 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts @@ -1,10 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, HostBinding, OnDestroy, OnInit } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, switchMap, takeUntil } from "rxjs"; 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"; import { DialogService } from "@bitwarden/components"; import { WebauthnLoginAdminService } from "../../core"; @@ -35,6 +37,7 @@ export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy { private webauthnService: WebauthnLoginAdminService, private dialogService: DialogService, private policyService: PolicyService, + private accountService: AccountService, ) {} @HostBinding("attr.aria-busy") @@ -57,9 +60,14 @@ export class WebauthnLoginSettingsComponent implements OnInit, OnDestroy { requireSsoPolicyEnabled = false; ngOnInit(): void { - this.policyService - .policyAppliesToActiveUser$(PolicyType.RequireSso) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.RequireSso, userId), + ), + takeUntil(this.destroy$), + ) .subscribe((enabled) => { this.requireSsoPolicyEnabled = enabled; }); 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 536e7b7020e..dc748e9ee41 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,7 +13,7 @@ import { } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom, map, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -28,6 +28,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction, BillingInformation, @@ -265,9 +266,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } this.upgradeFlowPrefillForm(); - this.policyService - .policyAppliesToActiveUser$(PolicyType.SingleOrg) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId), + ), + takeUntil(this.destroy$), + ) .subscribe((policyAppliesToActiveUser) => { this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser; }); 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 84f2467b1bd..fc7d6793a85 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -12,7 +12,7 @@ import { import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; import { Subject, firstValueFrom, takeUntil } from "rxjs"; -import { debounceTime, map } from "rxjs/operators"; +import { debounceTime, map, switchMap } from "rxjs/operators"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -31,6 +31,7 @@ import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/mode 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 { getUserId } from "@bitwarden/common/auth/services/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"; @@ -240,9 +241,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formGroup.controls.billingEmail.addValidators(Validators.required); } - this.policyService - .policyAppliesToActiveUser$(PolicyType.SingleOrg) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId), + ), + takeUntil(this.destroy$), + ) .subscribe((policyAppliesToActiveUser) => { this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser; }); 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 da569ffc993..8d4e89d40a0 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 @@ -96,7 +96,7 @@ export class FreeFamiliesPolicyService { return this.accountService.activeAccount$.pipe( getUserId, switchMap((userId) => - this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + this.policyService.policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), ), map((policies) => ({ isFreeFamilyPolicyEnabled: policies.some( 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 c35fd3a2e61..cee08bae8cd 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -89,7 +89,7 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { this.availableSponsorshipOrgs$ = combineLatest([ this.organizationService.organizations$(userId), - this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + this.policyService.policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), ]).pipe( map(([organizations, policies]) => organizations diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index e613b862922..c4e22c0a7e1 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -10,7 +10,6 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -36,7 +35,6 @@ export class SponsoringOrgRowComponent implements OnInit { private logService: LogService, private dialogService: DialogService, private toastService: ToastService, - private configService: ConfigService, private policyService: PolicyService, private accountService: AccountService, ) {} @@ -54,7 +52,7 @@ export class SponsoringOrgRowComponent implements OnInit { this.isFreeFamilyPolicyEnabled$ = this.accountService.activeAccount$.pipe( getUserId, switchMap((userId) => - this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), + this.policyService.policiesByType$(PolicyType.FreeFamiliesSponsorshipPolicy, userId), ), map( (policies) => diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts index 873ceea2ada..27ce4dc9f5d 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -4,7 +4,7 @@ import { StepperSelectionEvent } from "@angular/cdk/stepper"; import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs"; import { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular"; import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common"; @@ -12,6 +12,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationBillingServiceAbstraction as OrganizationBillingService, OrganizationInformation, @@ -106,6 +108,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { private validationService: ValidationService, private loginStrategyService: LoginStrategyServiceAbstraction, private configService: ConfigService, + private accountService: AccountService, ) {} async ngOnInit(): Promise { @@ -173,9 +176,12 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { } if (policies !== null) { - this.policyService - .masterPasswordPolicyOptions$(policies) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)), + takeUntil(this.destroy$), + ) .subscribe((enforcedPasswordPolicyOptions) => { this.enforcedPolicyOptions = enforcedPasswordPolicyOptions; }); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index cc1e481d39b..cc9024490d6 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -256,6 +256,7 @@ const safeProviders: SafeProvider[] = [ PolicyApiServiceAbstraction, LogService, PolicyService, + AccountService, ], }), safeProvider({ @@ -311,6 +312,7 @@ const safeProviders: SafeProvider[] = [ PlatformUtilsService, SsoLoginServiceAbstraction, Router, + AccountService, ], }), safeProvider({ diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index aedad9b26ea..d4a9c866fba 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -1,10 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; +import { switchMap } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DeviceType, EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; @@ -19,10 +22,16 @@ export class EventService { private i18nService: I18nService, policyService: PolicyService, private configService: ConfigService, + private accountService: AccountService, ) { - policyService.policies$.subscribe((policies) => { - this.policies = policies; - }); + accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => policyService.policies$(userId)), + ) + .subscribe((policies) => { + this.policies = policies; + }); } getDefaultDateFilters() { diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index a90f1d18afd..4d4e0c3d711 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -2,11 +2,23 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, tap } from "rxjs"; +import { + concatMap, + filter, + firstValueFrom, + map, + Observable, + Subject, + switchMap, + takeUntil, + tap, +} from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { VaultTimeout, @@ -100,7 +112,12 @@ export class PreferencesComponent implements OnInit, OnDestroy { this.availableVaultTimeoutActions$ = this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(); - this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe( + this.vaultTimeoutPolicyCallout = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId), + ), + getFirstPolicy, filter((policy) => policy != null), map((policy) => { let timeout; 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 37e3aca6cd5..4b9791c61bf 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 @@ -75,8 +75,10 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { - const resetPasswordPolicies$ = this.policyService.policies$.pipe( - map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)), + const resetPasswordPolicies$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policies$(userId)), + map((policies) => policies.filter((p) => p.type == PolicyType.ResetPassword)), ); const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 786c5de740e..0a168157705 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -6,6 +6,9 @@ import { firstValueFrom, merge, Subject, switchMap, takeUntil } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; +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/billing-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -101,6 +104,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { protected billingApiService: BillingApiServiceAbstraction, protected dialogService: DialogService, protected configService: ConfigService, + protected accountService: AccountService, ) {} async ngOnInit(): Promise { @@ -110,10 +114,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy { this.isLoaded = true; // Without refactoring the entire component, we need to manually update the organization filter whenever the policies update - merge( - this.policyService.get$(PolicyType.SingleOrg), - this.policyService.get$(PolicyType.PersonalOwnership), - ) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + merge( + this.policyService.policiesByType$(PolicyType.SingleOrg, userId).pipe(getFirstPolicy), + this.policyService + .policiesByType$(PolicyType.PersonalOwnership, userId) + .pipe(getFirstPolicy), + ), + ), + ) .pipe( switchMap(() => this.addOrganizationFilter()), takeUntil(this.destroy$), @@ -190,9 +202,22 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } protected async addOrganizationFilter(): Promise { - const singleOrgPolicy = await this.policyService.policyAppliesToUser(PolicyType.SingleOrg); - const personalVaultPolicy = await this.policyService.policyAppliesToUser( - PolicyType.PersonalOwnership, + const singleOrgPolicy = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId), + ), + ), + ); + + const personalVaultPolicy = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + ), ); const addAction = !singleOrgPolicy 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 15f5e1cd876..f56931fa987 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 @@ -65,11 +65,11 @@ describe("vault filter service", () => { organizationService.memberOrganizations$.mockReturnValue(organizations); folderService.folderViews$.mockReturnValue(folderViews); collectionService.decryptedCollections$ = collectionViews; - policyService.policyAppliesToActiveUser$ - .calledWith(PolicyType.PersonalOwnership) + policyService.policyAppliesToUser$ + .calledWith(PolicyType.PersonalOwnership, mockUserId) .mockReturnValue(personalOwnershipPolicy); - policyService.policyAppliesToActiveUser$ - .calledWith(PolicyType.SingleOrg) + policyService.policyAppliesToUser$ + .calledWith(PolicyType.SingleOrg, mockUserId) .mockReturnValue(singleOrgPolicy); cipherService.cipherViews$.mockReturnValue(cipherViews); 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 67c369fa0f2..f3e4441af9f 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 @@ -55,8 +55,14 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { organizationTree$: Observable> = combineLatest([ this.memberOrganizations$, - this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg), - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + this.activeUserId$.pipe( + switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)), + ), + this.activeUserId$.pipe( + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + ), ]).pipe( switchMap(([orgs, singleOrgPolicy, personalOwnershipPolicy]) => this.buildOrganizationTree(orgs, singleOrgPolicy, personalOwnershipPolicy), diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts index 1a767bc8964..5ec045f7be8 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts @@ -142,13 +142,13 @@ describe("VaultOnboardingComponent", () => { }); describe("individualVaultPolicyCheck", () => { - it("should set isIndividualPolicyVault to true", async () => { + it("should set isIndividualPolicyVault to true", () => { individualVaultPolicyCheckSpy.mockRestore(); const spy = jest - .spyOn((component as any).policyService, "policyAppliesToActiveUser$") + .spyOn((component as any).policyService, "policyAppliesToUser$") .mockReturnValue(of(true)); - await component.individualVaultPolicyCheck(); + component.individualVaultPolicyCheck(); fixture.detectChanges(); expect(spy).toHaveBeenCalled(); }); diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index dc4a014073a..b3a4b324d30 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -11,7 +11,7 @@ import { SimpleChanges, OnChanges, } from "@angular/core"; -import { Subject, takeUntil, Observable, firstValueFrom, fromEvent } from "rxjs"; +import { Subject, takeUntil, Observable, firstValueFrom, fromEvent, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -20,7 +20,6 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; @@ -67,7 +66,6 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { protected policyService: PolicyService, private apiService: ApiService, private vaultOnboardingService: VaultOnboardingServiceAbstraction, - private configService: ConfigService, private accountService: AccountService, ) {} @@ -165,9 +163,14 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { } individualVaultPolicyCheck() { - this.policyService - .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + takeUntil(this.destroy$), + ) .subscribe((data) => { this.isIndividualPolicyVault = data; }); 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 9ee13bf077a..0934a6deb95 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 @@ -38,7 +38,7 @@ describe("AdminConsoleCipherFormConfigService", () => { status: OrganizationUserStatusType.Confirmed, userId: "UserId", }; - const policyAppliesToActiveUser$ = new BehaviorSubject(true); + const policyAppliesToUser$ = new BehaviorSubject(true); const collection = { id: "12345-5555", organizationId: "234534-34334", @@ -75,7 +75,7 @@ describe("AdminConsoleCipherFormConfigService", () => { }, { provide: PolicyService, - useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ }, + useValue: { policyAppliesToUser$: () => policyAppliesToUser$ }, }, { provide: RoutedVaultFilterService, @@ -129,13 +129,13 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("sets `allowPersonalOwnership`", async () => { - policyAppliesToActiveUser$.next(true); + policyAppliesToUser$.next(true); let result = await adminConsoleConfigService.buildConfig("clone", cipherId); expect(result.allowPersonalOwnership).toBe(false); - policyAppliesToActiveUser$.next(false); + policyAppliesToUser$.next(false); result = await adminConsoleConfigService.buildConfig("clone", cipherId); @@ -143,7 +143,7 @@ describe("AdminConsoleCipherFormConfigService", () => { }); it("disables personal ownership when not cloning", async () => { - policyAppliesToActiveUser$.next(false); + policyAppliesToUser$.next(false); let result = await adminConsoleConfigService.buildConfig("add", cipherId); 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 19259ba4033..dd9cef91a54 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 @@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -30,9 +31,13 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ private apiService: ApiService = inject(ApiService); private accountService: AccountService = inject(AccountService); - private allowPersonalOwnership$ = this.policyService - .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) - .pipe(map((p) => !p)); + private allowPersonalOwnership$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + map((p) => !p), + ); private organizationId$ = this.routedVaultFilterService.filter$.pipe( map((filter) => filter.organizationId), diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts index 70823d61c39..d346b7ba9ba 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts @@ -8,6 +8,7 @@ import { map, Observable, Subject, + switchMap, take, takeUntil, withLatestFrom, @@ -18,6 +19,8 @@ import { OrgDomainServiceAbstraction } from "@bitwarden/common/admin-console/abs import { OrganizationDomainResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain.response"; 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"; import { HttpStatusCode } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -55,6 +58,7 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private policyService: PolicyService, + private accountService: AccountService, ) { this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.AccountDeprovisioning, @@ -83,7 +87,9 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { if (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) { const singleOrgPolicy = await firstValueFrom( - this.policyService.policies$.pipe( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policies$(userId)), map((policies) => policies.find( (p) => p.type === PolicyType.SingleOrg && p.organizationId === this.organizationId, diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 2582d6a7103..3b186a7fd2e 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.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, OnDestroy, OnInit } from "@angular/core"; -import { Subject, firstValueFrom, map, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -52,9 +53,12 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { this.email = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), ); - this.policyService - .masterPasswordPolicyOptions$() - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + takeUntil(this.destroy$), + ) .subscribe( (enforcedPasswordPolicyOptions) => (this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions), diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 4672788eb81..3a28f28caaf 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -75,12 +75,13 @@ import { OrganizationApiService } from "@bitwarden/common/admin-console/services 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"; +import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.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"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { + AccountService, AccountService as AccountServiceAbstraction, InternalAccountService, } from "@bitwarden/common/auth/abstractions/account.service"; @@ -947,7 +948,7 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: InternalPolicyService, - useClass: PolicyService, + useClass: DefaultPolicyService, deps: [StateProvider, OrganizationServiceAbstraction], }), safeProvider({ @@ -957,7 +958,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: PolicyApiServiceAbstraction, useClass: PolicyApiService, - deps: [InternalPolicyService, ApiServiceAbstraction], + deps: [InternalPolicyService, ApiServiceAbstraction, AccountService], }), safeProvider({ provide: InternalMasterPasswordServiceAbstraction, @@ -1259,7 +1260,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: AutofillSettingsServiceAbstraction, useClass: AutofillSettingsService, - deps: [StateProvider, PolicyServiceAbstraction], + deps: [StateProvider, PolicyServiceAbstraction, AccountService], }), safeProvider({ provide: BadgeSettingsServiceAbstraction, diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index 4f7d4b6b600..7e6180e5849 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -155,9 +155,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.policyService - .policyAppliesToActiveUser$(PolicyType.DisableSend) - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId), + ), + takeUntil(this.destroy$), + ) .subscribe((policyAppliesToActiveUser) => { this.disableSend = policyAppliesToActiveUser; if (this.disableSend) { @@ -168,7 +173,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.accountService.activeAccount$ .pipe( getUserId, - switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), + switchMap((userId) => this.policyService.policiesByType$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntil(this.destroy$), ) diff --git a/libs/angular/src/tools/send/send.component.ts b/libs/angular/src/tools/send/send.component.ts index 738960fc628..5dbf3686b7d 100644 --- a/libs/angular/src/tools/send/send.component.ts +++ b/libs/angular/src/tools/send/send.component.ts @@ -9,6 +9,7 @@ import { from, switchMap, takeUntil, + combineLatest, } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -85,18 +86,23 @@ export class SendComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - - this.policyService - .policyAppliesToActiveUser$(PolicyType.DisableSend) - .pipe(takeUntil(this.destroy$)) - .subscribe((policyAppliesToActiveUser) => { - this.disableSend = policyAppliesToActiveUser; + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId), + ), + takeUntil(this.destroy$), + ) + .subscribe((policyAppliesToUser) => { + this.disableSend = policyAppliesToUser; }); - this._searchText$ + combineLatest([this._searchText$, this.accountService.activeAccount$.pipe(getUserId)]) .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(userId, searchText))), + switchMap(([searchText, userId]) => + from(this.searchService.isSearchable(userId, searchText)), + ), takeUntil(this.destroy$), ) .subscribe((isSearchable) => { diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index eed90c2ba70..2393863bb5f 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { DatePipe } from "@angular/common"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { concatMap, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, map, Observable, Subject, switchMap, takeUntil } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -193,9 +193,12 @@ export class AddEditComponent implements OnInit, OnDestroy { } async ngOnInit() { - this.policyService - .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), concatMap(async (policyAppliesToActiveUser) => { this.personalOwnershipPolicyAppliesToActiveUser = policyAppliesToActiveUser; await this.init(); 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 01fa3384b82..6c3ac21b162 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 @@ -112,13 +112,23 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti async checkForSingleOrganizationPolicy(): Promise { return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId), + ), + ), ); } async checkForPersonalOwnershipPolicy(): Promise { return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + ), ); } diff --git a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts index d510d671d69..05bb97dde26 100644 --- a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts +++ b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts @@ -2,25 +2,32 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { BehaviorSubject } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { VaultTimeoutSettingsService, VaultTimeoutStringType, } from "@bitwarden/common/key-management/vault-timeout"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { VaultTimeoutInputComponent } from "./vault-timeout-input.component"; describe("VaultTimeoutInputComponent", () => { let component: VaultTimeoutInputComponent; let fixture: ComponentFixture; - const get$ = jest.fn().mockReturnValue(new BehaviorSubject({})); + const policiesByType$ = jest.fn().mockReturnValue(new BehaviorSubject({})); const availableVaultTimeoutActions$ = jest.fn().mockReturnValue(new BehaviorSubject([])); + const mockUserId = Utils.newGuid() as UserId; + const accountService = mockAccountServiceWith(mockUserId); beforeEach(async () => { await TestBed.configureTestingModule({ imports: [VaultTimeoutInputComponent], providers: [ - { provide: PolicyService, useValue: { get$ } }, + { provide: PolicyService, useValue: { policiesByType$ } }, + { provide: AccountService, useValue: accountService }, { provide: VaultTimeoutSettingsService, useValue: { availableVaultTimeoutActions$ } }, { provide: I18nService, useValue: { t: (key: string) => key } }, ], diff --git a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts index 91af1e8adbe..82bc53bb147 100644 --- a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts +++ b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts @@ -14,12 +14,15 @@ import { ValidationErrors, Validator, } from "@angular/forms"; -import { filter, map, Observable, Subject, takeUntil } from "rxjs"; +import { filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { VaultTimeout, VaultTimeoutAction, @@ -123,12 +126,17 @@ export class VaultTimeoutInputComponent private policyService: PolicyService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private i18nService: I18nService, + private accountService: AccountService, ) {} async ngOnInit() { - this.policyService - .get$(PolicyType.MaximumVaultTimeout) + this.accountService.activeAccount$ .pipe( + getUserId, + switchMap((userId) => + this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId), + ), + getFirstPolicy, filter((policy) => policy != null), takeUntil(this.destroy$), ) @@ -136,7 +144,6 @@ export class VaultTimeoutInputComponent this.vaultTimeoutPolicy = policy; this.applyVaultTimeoutPolicy(); }); - this.form.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((value: VaultTimeoutFormValue) => { diff --git a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts index 18cc13b29c9..4db0fc16750 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ListResponse } from "../../../models/response/list.response"; import { PolicyType } from "../../enums"; import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options"; @@ -7,19 +5,23 @@ import { Policy } from "../../models/domain/policy"; import { PolicyRequest } from "../../models/request/policy.request"; import { PolicyResponse } from "../../models/response/policy.response"; -export class PolicyApiServiceAbstraction { - getPolicy: (organizationId: string, type: PolicyType) => Promise; - getPolicies: (organizationId: string) => Promise>; +export abstract class PolicyApiServiceAbstraction { + abstract getPolicy: (organizationId: string, type: PolicyType) => Promise; + abstract getPolicies: (organizationId: string) => Promise>; - getPoliciesByToken: ( + abstract getPoliciesByToken: ( organizationId: string, token: string, email: string, organizationUserId: string, ) => Promise; - getMasterPasswordPolicyOptsForOrgUser: ( + abstract getMasterPasswordPolicyOptsForOrgUser: ( orgId: string, ) => Promise; - putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise; + abstract putPolicy: ( + organizationId: string, + type: PolicyType, + request: PolicyRequest, + ) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index 4280756326c..68f9843c5bd 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../../types/guid"; @@ -11,43 +9,27 @@ import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-p export abstract class PolicyService { /** - * All policies for the active user from sync data. + * All policies for the provided user from sync data. * May include policies that are disabled or otherwise do not apply to the user. Be careful using this! - * Consider using {@link get$} or {@link getAll$} instead, which will only return policies that should be enforced against the user. + * Consider {@link policiesByType$} instead, which will only return policies that should be enforced against the user. */ - policies$: Observable; + abstract policies$: (userId: UserId) => Observable; /** - * @returns the first {@link Policy} found that applies to the active user. + * @returns all {@link Policy} objects of a given type that apply to the specified user. * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * @param policyType the {@link PolicyType} to search for - * @see {@link getAll$} if you need all policies of a given type + * @param userId the {@link UserId} to search against */ - get$: (policyType: PolicyType) => Observable; + abstract policiesByType$: (policyType: PolicyType, userId: UserId) => Observable; /** - * @returns all {@link Policy} objects of a given type that apply to the specified user (or the active user if not specified). + * @returns true if a policy of the specified type applies to the specified user, otherwise false. * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). - * @param policyType the {@link PolicyType} to search for - */ - getAll$: (policyType: PolicyType, userId: UserId) => Observable; - - /** - * All {@link Policy} objects for the specified user (from sync data). - * May include policies that are disabled or otherwise do not apply to the user. - * Consider using {@link getAll$} instead, which will only return policies that should be enforced against the user. - */ - getAll: (policyType: PolicyType) => Promise; - - /** - * @returns true if a policy of the specified type applies to the active user, otherwise false. - * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). - * This does not take into account the policy's configuration - if that is important, use {@link getAll$} to get the + * This does not take into account the policy's configuration - if that is important, use {@link policiesByType$} to get the * {@link Policy} objects and then filter by Policy.data. */ - policyAppliesToActiveUser$: (policyType: PolicyType) => Observable; - - policyAppliesToUser: (policyType: PolicyType) => Promise; + abstract policyAppliesToUser$: (policyType: PolicyType, userId: UserId) => Observable; // Policy specific interfaces @@ -56,28 +38,31 @@ export abstract class PolicyService { * @returns a set of options which represent the minimum Master Password settings that the user must * comply with in order to comply with **all** Master Password policies. */ - masterPasswordPolicyOptions$: (policies?: Policy[]) => Observable; + abstract masterPasswordPolicyOptions$: ( + userId: UserId, + policies?: Policy[], + ) => Observable; /** * Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user. */ - evaluateMasterPassword: ( + abstract evaluateMasterPassword: ( passwordStrength: number, newPassword: string, enforcedPolicyOptions?: MasterPasswordPolicyOptions, ) => boolean; /** - * @returns Reset Password policy options for the specified organization and a boolean indicating whether the policy + * @returns {@link ResetPasswordPolicyOptions} for the specified organization and a boolean indicating whether the policy * is enabled */ - getResetPasswordPolicyOptions: ( + abstract getResetPasswordPolicyOptions: ( policies: Policy[], orgId: string, ) => [ResetPasswordPolicyOptions, boolean]; } export abstract class InternalPolicyService extends PolicyService { - upsert: (policy: PolicyData) => Promise; - replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise; + abstract upsert: (policy: PolicyData, userId: UserId) => Promise; + abstract replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/policy/vnext-policy.service.ts b/libs/common/src/admin-console/abstractions/policy/vnext-policy.service.ts deleted file mode 100644 index 1e96d6b8d00..00000000000 --- a/libs/common/src/admin-console/abstractions/policy/vnext-policy.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; -import { PolicyType } from "../../enums"; -import { PolicyData } from "../../models/data/policy.data"; -import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options"; -import { Policy } from "../../models/domain/policy"; -import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-policy-options"; - -export abstract class vNextPolicyService { - /** - * All policies for the provided user from sync data. - * May include policies that are disabled or otherwise do not apply to the user. Be careful using this! - * Consider {@link policiesByType$} instead, which will only return policies that should be enforced against the user. - */ - abstract policies$: (userId: UserId) => Observable; - - /** - * @returns all {@link Policy} objects of a given type that apply to the specified user. - * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). - * @param policyType the {@link PolicyType} to search for - * @param userId the {@link UserId} to search against - */ - abstract policiesByType$: (policyType: PolicyType, userId: UserId) => Observable; - - /** - * @returns true if a policy of the specified type applies to the specified user, otherwise false. - * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). - * This does not take into account the policy's configuration - if that is important, use {@link policiesByType$} to get the - * {@link Policy} objects and then filter by Policy.data. - */ - abstract policyAppliesToUser$: (policyType: PolicyType, userId: UserId) => Observable; - - // Policy specific interfaces - - /** - * Combines all Master Password policies that apply to the user. - * @returns a set of options which represent the minimum Master Password settings that the user must - * comply with in order to comply with **all** Master Password policies. - */ - abstract masterPasswordPolicyOptions$: ( - userId: UserId, - policies?: Policy[], - ) => Observable; - - /** - * Evaluates whether a proposed Master Password complies with all Master Password policies that apply to the user. - */ - abstract evaluateMasterPassword: ( - passwordStrength: number, - newPassword: string, - enforcedPolicyOptions?: MasterPasswordPolicyOptions, - ) => boolean; - - /** - * @returns {@link ResetPasswordPolicyOptions} for the specified organization and a boolean indicating whether the policy - * is enabled - */ - abstract getResetPasswordPolicyOptions: ( - policies: Policy[], - orgId: string, - ) => [ResetPasswordPolicyOptions, boolean]; -} - -export abstract class vNextInternalPolicyService extends vNextPolicyService { - abstract upsert: (policy: PolicyData, userId: UserId) => Promise; - abstract replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise; -} diff --git a/libs/common/src/admin-console/services/policy/default-vnext-policy.service.spec.ts b/libs/common/src/admin-console/services/policy/default-policy.service.spec.ts similarity index 98% rename from libs/common/src/admin-console/services/policy/default-vnext-policy.service.spec.ts rename to libs/common/src/admin-console/services/policy/default-policy.service.spec.ts index f58e1d27ee6..7787bdbc943 100644 --- a/libs/common/src/admin-console/services/policy/default-vnext-policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/default-policy.service.spec.ts @@ -15,11 +15,11 @@ import { MasterPasswordPolicyOptions } from "../../../admin-console/models/domai import { Organization } from "../../../admin-console/models/domain/organization"; import { Policy } from "../../../admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain/reset-password-policy-options"; -import { POLICIES } from "../../../admin-console/services/policy/policy.service"; import { PolicyId, UserId } from "../../../types/guid"; import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; -import { DefaultvNextPolicyService, getFirstPolicy } from "./default-vnext-policy.service"; +import { DefaultPolicyService, getFirstPolicy } from "./default-policy.service"; +import { POLICIES } from "./policy-state"; describe("PolicyService", () => { const userId = "userId" as UserId; @@ -27,7 +27,7 @@ describe("PolicyService", () => { let organizationService: MockProxy; let singleUserState: FakeSingleUserState>; - let policyService: DefaultvNextPolicyService; + let policyService: DefaultPolicyService; beforeEach(() => { const accountService = mockAccountServiceWith(userId); @@ -59,7 +59,7 @@ describe("PolicyService", () => { organizationService.organizations$.calledWith(userId).mockReturnValue(organizations$); - policyService = new DefaultvNextPolicyService(stateProvider, organizationService); + policyService = new DefaultPolicyService(stateProvider, organizationService); }); it("upsert", async () => { diff --git a/libs/common/src/admin-console/services/policy/default-vnext-policy.service.ts b/libs/common/src/admin-console/services/policy/default-policy.service.ts similarity index 95% rename from libs/common/src/admin-console/services/policy/default-vnext-policy.service.ts rename to libs/common/src/admin-console/services/policy/default-policy.service.ts index bc56638a987..1158d29d737 100644 --- a/libs/common/src/admin-console/services/policy/default-vnext-policy.service.ts +++ b/libs/common/src/admin-console/services/policy/default-policy.service.ts @@ -3,7 +3,7 @@ import { combineLatest, map, Observable, of } from "rxjs"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; -import { vNextPolicyService } from "../../abstractions/policy/vnext-policy.service"; +import { PolicyService } from "../../abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "../../enums"; import { PolicyData } from "../../models/data/policy.data"; import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options"; @@ -11,7 +11,7 @@ import { Organization } from "../../models/domain/organization"; import { Policy } from "../../models/domain/policy"; import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-policy-options"; -import { POLICIES } from "./vnext-policy-state"; +import { POLICIES } from "./policy-state"; export function policyRecordToArray(policiesMap: { [id: string]: PolicyData }) { return Object.values(policiesMap || {}).map((f) => new Policy(f)); @@ -21,7 +21,7 @@ export const getFirstPolicy = map((policies) => { return policies.at(0) ?? undefined; }); -export class DefaultvNextPolicyService implements vNextPolicyService { +export class DefaultPolicyService implements PolicyService { constructor( private stateProvider: StateProvider, private organizationService: OrganizationService, @@ -89,7 +89,7 @@ export class DefaultvNextPolicyService implements vNextPolicyService { const policies$ = policies ? of(policies) : this.policies$(userId); return policies$.pipe( map((obsPolicies) => { - const enforcedOptions: MasterPasswordPolicyOptions = new MasterPasswordPolicyOptions(); + let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined; const filteredPolicies = obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? []; @@ -102,6 +102,10 @@ export class DefaultvNextPolicyService implements vNextPolicyService { return; } + if (!enforcedOptions) { + enforcedOptions = new MasterPasswordPolicyOptions(); + } + if ( currentPolicy.data.minComplexity != null && currentPolicy.data.minComplexity > enforcedOptions.minComplexity diff --git a/libs/common/src/admin-console/services/policy/policy-api.service.ts b/libs/common/src/admin-console/services/policy/policy-api.service.ts index 086bbea1d21..8f9854f49c4 100644 --- a/libs/common/src/admin-console/services/policy/policy-api.service.ts +++ b/libs/common/src/admin-console/services/policy/policy-api.service.ts @@ -1,6 +1,8 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { ApiService } from "../../../abstractions/api.service"; +import { AccountService } from "../../../auth/abstractions/account.service"; +import { getUserId } from "../../../auth/services/account.service"; import { HttpStatusCode } from "../../../enums"; import { ErrorResponse } from "../../../models/response/error.response"; import { ListResponse } from "../../../models/response/list.response"; @@ -18,6 +20,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction { constructor( private policyService: InternalPolicyService, private apiService: ApiService, + private accountService: AccountService, ) {} async getPolicy(organizationId: string, type: PolicyType): Promise { @@ -93,8 +96,14 @@ export class PolicyApiService implements PolicyApiServiceAbstraction { return null; } - return await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$([masterPasswordPolicy]), + return firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.masterPasswordPolicyOptions$(userId, [masterPasswordPolicy]), + ), + map((policy) => policy ?? null), + ), ); } catch (error) { // If policy not found, return null @@ -114,8 +123,9 @@ export class PolicyApiService implements PolicyApiServiceAbstraction { true, true, ); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const response = new PolicyResponse(r); const data = new PolicyData(response); - await this.policyService.upsert(data); + await this.policyService.upsert(data, userId); } } diff --git a/libs/common/src/admin-console/services/policy/vnext-policy-state.ts b/libs/common/src/admin-console/services/policy/policy-state.ts similarity index 100% rename from libs/common/src/admin-console/services/policy/vnext-policy-state.ts rename to libs/common/src/admin-console/services/policy/policy-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 deleted file mode 100644 index 48979f1e31e..00000000000 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ /dev/null @@ -1,556 +0,0 @@ -import { mock, MockProxy } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; - -import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { FakeActiveUserState, FakeSingleUserState } from "../../../../spec/fake-state"; -import { - OrganizationUserStatusType, - OrganizationUserType, - PolicyType, -} from "../../../admin-console/enums"; -import { PermissionsApi } from "../../../admin-console/models/api/permissions.api"; -import { OrganizationData } from "../../../admin-console/models/data/organization.data"; -import { PolicyData } from "../../../admin-console/models/data/policy.data"; -import { MasterPasswordPolicyOptions } from "../../../admin-console/models/domain/master-password-policy-options"; -import { Organization } from "../../../admin-console/models/domain/organization"; -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; - let stateProvider: FakeStateProvider; - let organizationService: MockProxy; - let activeUserState: FakeActiveUserState>; - let singleUserState: FakeSingleUserState>; - - let policyService: PolicyService; - - beforeEach(() => { - const accountService = mockAccountServiceWith(userId); - stateProvider = new FakeStateProvider(accountService); - organizationService = mock(); - - activeUserState = stateProvider.activeUser.getFake(POLICIES); - singleUserState = stateProvider.singleUser.getFake(activeUserState.userId, POLICIES); - - const organizations$ = of([ - // User - organization("org1", true, true, OrganizationUserStatusType.Confirmed, false), - // Owner - organization( - "org2", - true, - true, - OrganizationUserStatusType.Confirmed, - false, - OrganizationUserType.Owner, - ), - // Does not use policies - organization("org3", true, false, OrganizationUserStatusType.Confirmed, false), - // Another User - organization("org4", true, true, OrganizationUserStatusType.Confirmed, false), - // Another User - organization("org5", true, true, OrganizationUserStatusType.Confirmed, false), - // Can manage policies - organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), - ]); - - organizationService.organizations$.mockReturnValue(organizations$); - - policyService = new PolicyService(stateProvider, organizationService); - }); - - it("upsert", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }), - ]), - ); - - await policyService.upsert(policyData("99", "test-organization", PolicyType.DisableSend, true)); - - expect(await firstValueFrom(policyService.policies$)).toEqual([ - { - id: "1", - organizationId: "test-organization", - type: PolicyType.MaximumVaultTimeout, - enabled: true, - data: { minutes: 14 }, - }, - { - id: "99", - organizationId: "test-organization", - type: PolicyType.DisableSend, - enabled: true, - }, - ]); - }); - - it("replace", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }), - ]), - ); - - await policyService.replace( - { - "2": policyData("2", "test-organization", PolicyType.DisableSend, true), - }, - userId, - ); - - expect(await firstValueFrom(policyService.policies$)).toEqual([ - { - id: "2", - organizationId: "test-organization", - type: PolicyType.DisableSend, - enabled: true, - }, - ]); - }); - - describe("masterPasswordPolicyOptions", () => { - it("returns default policy options", async () => { - const data: any = { - minComplexity: 5, - minLength: 20, - requireUpper: true, - }; - const model = [ - new Policy(policyData("1", "test-organization-3", PolicyType.MasterPassword, true, data)), - ]; - const result = await firstValueFrom(policyService.masterPasswordPolicyOptions$(model)); - - expect(result).toEqual({ - minComplexity: 5, - minLength: 20, - requireLower: false, - requireNumbers: false, - requireSpecial: false, - requireUpper: true, - enforceOnLogin: false, - }); - }); - - it("returns null", async () => { - const data: any = {}; - const model = [ - new Policy( - policyData("3", "test-organization-3", PolicyType.DisablePersonalVaultExport, true, data), - ), - new Policy( - policyData("4", "test-organization-3", PolicyType.MaximumVaultTimeout, true, data), - ), - ]; - - const result = await firstValueFrom(policyService.masterPasswordPolicyOptions$(model)); - - expect(result).toEqual(null); - }); - - it("returns specified policy options", async () => { - const data: any = { - minLength: 14, - }; - const model = [ - new Policy( - policyData("3", "test-organization-3", PolicyType.DisablePersonalVaultExport, true, data), - ), - new Policy(policyData("4", "test-organization-3", PolicyType.MasterPassword, true, data)), - ]; - - const result = await firstValueFrom(policyService.masterPasswordPolicyOptions$(model)); - - expect(result).toEqual({ - minComplexity: 0, - minLength: 14, - requireLower: false, - requireNumbers: false, - requireSpecial: false, - requireUpper: false, - enforceOnLogin: false, - }); - }); - }); - - describe("evaluateMasterPassword", () => { - it("false", async () => { - const enforcedPolicyOptions = new MasterPasswordPolicyOptions(); - enforcedPolicyOptions.minLength = 14; - const result = policyService.evaluateMasterPassword(10, "password", enforcedPolicyOptions); - - expect(result).toEqual(false); - }); - - it("true", async () => { - const enforcedPolicyOptions = new MasterPasswordPolicyOptions(); - const result = policyService.evaluateMasterPassword(0, "password", enforcedPolicyOptions); - - expect(result).toEqual(true); - }); - }); - - describe("getResetPasswordPolicyOptions", () => { - it("default", async () => { - const result = policyService.getResetPasswordPolicyOptions([], ""); - - expect(result).toEqual([new ResetPasswordPolicyOptions(), false]); - }); - - it("returns autoEnrollEnabled true", async () => { - const data: any = { - autoEnrollEnabled: true, - }; - const policies = [ - new Policy(policyData("5", "test-organization-3", PolicyType.ResetPassword, true, data)), - ]; - const result = policyService.getResetPasswordPolicyOptions(policies, "test-organization-3"); - - expect(result).toEqual([{ autoEnrollEnabled: true }, true]); - }); - }); - - describe("get$", () => { - it("returns the specified PolicyType", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy1", "org1", PolicyType.ActivateAutofill, true), - policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true), - policyData("policy3", "org1", PolicyType.RemoveUnlockWithPin, true), - ]), - ); - - await expect( - firstValueFrom(policyService.get$(PolicyType.ActivateAutofill)), - ).resolves.toMatchObject({ - id: "policy1", - organizationId: "org1", - type: PolicyType.ActivateAutofill, - enabled: true, - }); - await expect( - firstValueFrom(policyService.get$(PolicyType.DisablePersonalVaultExport)), - ).resolves.toMatchObject({ - id: "policy2", - organizationId: "org1", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }); - await expect( - firstValueFrom(policyService.get$(PolicyType.RemoveUnlockWithPin)), - ).resolves.toMatchObject({ - id: "policy3", - organizationId: "org1", - type: PolicyType.RemoveUnlockWithPin, - enabled: true, - }); - }); - - it("does not return disabled policies", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy1", "org1", PolicyType.ActivateAutofill, true), - policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, false), - ]), - ); - - const result = await firstValueFrom( - policyService.get$(PolicyType.DisablePersonalVaultExport), - ); - - expect(result).toBeNull(); - }); - - it("does not return policies that do not apply to the user because the user's role is exempt", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy1", "org1", PolicyType.ActivateAutofill, true), - policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, false), - ]), - ); - - const result = await firstValueFrom( - policyService.get$(PolicyType.DisablePersonalVaultExport), - ); - - expect(result).toBeNull(); - }); - - it.each([ - ["owners", "org2"], - ["administrators", "org6"], - ])("returns the password generator policy for %s", async (_, organization) => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy1", "org1", PolicyType.ActivateAutofill, false), - policyData("policy2", organization, PolicyType.PasswordGenerator, true), - ]), - ); - - const result = await firstValueFrom(policyService.get$(PolicyType.PasswordGenerator)); - - expect(result).toBeTruthy(); - }); - - it("does not return policies for organizations that do not use policies", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy1", "org3", PolicyType.ActivateAutofill, true), - policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, true), - ]), - ); - - const result = await firstValueFrom(policyService.get$(PolicyType.ActivateAutofill)); - - expect(result).toBeNull(); - }); - }); - - describe("getAll$", () => { - it("returns the specified PolicyTypes", async () => { - singleUserState.nextState( - arrayToRecord([ - policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true), - policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true), - ]), - ); - - const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), - ); - - expect(result).toEqual([ - { - id: "policy1", - organizationId: "org4", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - { - id: "policy3", - organizationId: "org5", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - { - id: "policy4", - organizationId: "org1", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - ]); - }); - - it("does not return disabled policies", async () => { - singleUserState.nextState( - arrayToRecord([ - policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled - policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true), - ]), - ); - - const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), - ); - - expect(result).toEqual([ - { - id: "policy1", - organizationId: "org4", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - { - id: "policy4", - organizationId: "org1", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - ]); - }); - - it("does not return policies that do not apply to the user because the user's role is exempt", async () => { - singleUserState.nextState( - arrayToRecord([ - policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true), - policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner - ]), - ); - - const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), - ); - - expect(result).toEqual([ - { - id: "policy1", - organizationId: "org4", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - { - id: "policy3", - organizationId: "org5", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - ]); - }); - - it("does not return policies for organizations that do not use policies", async () => { - singleUserState.nextState( - arrayToRecord([ - policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies - policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true), - ]), - ); - - const result = await firstValueFrom( - policyService.getAll$(PolicyType.DisablePersonalVaultExport, activeUserState.userId), - ); - - expect(result).toEqual([ - { - id: "policy1", - organizationId: "org4", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - { - id: "policy4", - organizationId: "org1", - type: PolicyType.DisablePersonalVaultExport, - enabled: true, - }, - ]); - }); - }); - - describe("policyAppliesToActiveUser$", () => { - it("returns true when the policyType applies to the user", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true), - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true), - policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true), - ]), - ); - - const result = await firstValueFrom( - policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport), - ); - - expect(result).toBe(true); - }); - - it("returns false when policyType is disabled", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled - ]), - ); - - const result = await firstValueFrom( - policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport), - ); - - expect(result).toBe(false); - }); - - it("returns false when the policyType does not apply to the user because the user's role is exempt", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner - ]), - ); - - const result = await firstValueFrom( - policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport), - ); - - expect(result).toBe(false); - }); - - it("returns false for organizations that do not use policies", async () => { - activeUserState.nextState( - arrayToRecord([ - policyData("policy2", "org1", PolicyType.ActivateAutofill, true), - policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies - ]), - ); - - const result = await firstValueFrom( - policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport), - ); - - expect(result).toBe(false); - }); - }); - - function policyData( - id: string, - organizationId: string, - type: PolicyType, - enabled: boolean, - data?: any, - ) { - const policyData = new PolicyData({} as any); - policyData.id = id as PolicyId; - policyData.organizationId = organizationId; - policyData.type = type; - policyData.enabled = enabled; - policyData.data = data; - - return policyData; - } - - function organizationData( - id: string, - enabled: boolean, - usePolicies: boolean, - status: OrganizationUserStatusType, - managePolicies: boolean, - type: OrganizationUserType = OrganizationUserType.User, - ) { - const organizationData = new OrganizationData({} as any, {} as any); - organizationData.id = id; - organizationData.enabled = enabled; - organizationData.usePolicies = usePolicies; - organizationData.status = status; - organizationData.permissions = new PermissionsApi({ managePolicies: managePolicies } as any); - organizationData.type = type; - return organizationData; - } - - function organization( - id: string, - enabled: boolean, - usePolicies: boolean, - status: OrganizationUserStatusType, - managePolicies: boolean, - type: OrganizationUserType = OrganizationUserType.User, - ) { - return new Organization( - organizationData(id, enabled, usePolicies, status, managePolicies, type), - ); - } - - function arrayToRecord(input: PolicyData[]): Record { - return Object.fromEntries(input.map((i) => [i.id, i])); - } -}); diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts deleted file mode 100644 index ed4c7970a78..00000000000 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ /dev/null @@ -1,257 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; - -import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; -import { PolicyId, UserId } from "../../../types/guid"; -import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; -import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction"; -import { OrganizationUserStatusType, PolicyType } from "../../enums"; -import { PolicyData } from "../../models/data/policy.data"; -import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options"; -import { Organization } from "../../models/domain/organization"; -import { Policy } from "../../models/domain/policy"; -import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-policy-options"; - -const policyRecordToArray = (policiesMap: { [id: string]: PolicyData }) => - Object.values(policiesMap || {}).map((f) => new Policy(f)); - -export const POLICIES = UserKeyDefinition.record(POLICIES_DISK, "policies", { - deserializer: (policyData) => policyData, - clearOn: ["logout"], -}); - -export class PolicyService implements InternalPolicyServiceAbstraction { - private activeUserPolicyState = this.stateProvider.getActive(POLICIES); - private activeUserPolicies$ = this.activeUserPolicyState.state$.pipe( - map((policyData) => policyRecordToArray(policyData)), - ); - - policies$ = this.activeUserPolicies$; - - constructor( - private stateProvider: StateProvider, - private organizationService: OrganizationService, - ) {} - - get$(policyType: PolicyType): Observable { - const filteredPolicies$ = this.activeUserPolicies$.pipe( - map((policies) => policies.filter((p) => p.type === policyType)), - ); - - 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, - ), - ); - } - - getAll$(policyType: PolicyType, userId: UserId) { - const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe( - map((policyData) => policyRecordToArray(policyData)), - map((policies) => policies.filter((p) => p.type === policyType)), - ); - - return combineLatest([filteredPolicies$, this.organizationService.organizations$(userId)]).pipe( - map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)), - ); - } - - async getAll(policyType: PolicyType) { - return await firstValueFrom( - this.policies$.pipe(map((policies) => policies.filter((p) => p.type === policyType))), - ); - } - - policyAppliesToActiveUser$(policyType: PolicyType) { - return this.get$(policyType).pipe(map((policy) => policy != null)); - } - - async policyAppliesToUser(policyType: PolicyType) { - return await firstValueFrom(this.policyAppliesToActiveUser$(policyType)); - } - - private enforcedPolicyFilter(policies: Policy[], organizations: Organization[]) { - const orgDict = Object.fromEntries(organizations.map((o) => [o.id, o])); - return policies.filter((policy) => { - const organization = orgDict[policy.organizationId]; - - // This shouldn't happen, i.e. the user should only have policies for orgs they are a member of - // But if it does, err on the side of enforcing the policy - if (organization == null) { - return true; - } - - return ( - policy.enabled && - organization.status >= OrganizationUserStatusType.Accepted && - organization.usePolicies && - !this.isExemptFromPolicy(policy.type, organization) - ); - }); - } - - masterPasswordPolicyOptions$(policies?: Policy[]): Observable { - const observable = policies ? of(policies) : this.policies$; - return observable.pipe( - map((obsPolicies) => { - let enforcedOptions: MasterPasswordPolicyOptions = null; - const filteredPolicies = obsPolicies.filter((p) => p.type === PolicyType.MasterPassword); - - if (filteredPolicies == null || filteredPolicies.length === 0) { - return enforcedOptions; - } - - filteredPolicies.forEach((currentPolicy) => { - if (!currentPolicy.enabled || currentPolicy.data == null) { - return; - } - - if (enforcedOptions == null) { - enforcedOptions = new MasterPasswordPolicyOptions(); - } - - if ( - currentPolicy.data.minComplexity != null && - currentPolicy.data.minComplexity > enforcedOptions.minComplexity - ) { - enforcedOptions.minComplexity = currentPolicy.data.minComplexity; - } - - if ( - currentPolicy.data.minLength != null && - currentPolicy.data.minLength > enforcedOptions.minLength - ) { - enforcedOptions.minLength = currentPolicy.data.minLength; - } - - if (currentPolicy.data.requireUpper) { - enforcedOptions.requireUpper = true; - } - - if (currentPolicy.data.requireLower) { - enforcedOptions.requireLower = true; - } - - if (currentPolicy.data.requireNumbers) { - enforcedOptions.requireNumbers = true; - } - - if (currentPolicy.data.requireSpecial) { - enforcedOptions.requireSpecial = true; - } - - if (currentPolicy.data.enforceOnLogin) { - enforcedOptions.enforceOnLogin = true; - } - }); - - return enforcedOptions; - }), - ); - } - - evaluateMasterPassword( - passwordStrength: number, - newPassword: string, - enforcedPolicyOptions: MasterPasswordPolicyOptions, - ): boolean { - if (enforcedPolicyOptions == null) { - return true; - } - - if ( - enforcedPolicyOptions.minComplexity > 0 && - enforcedPolicyOptions.minComplexity > passwordStrength - ) { - return false; - } - - if ( - enforcedPolicyOptions.minLength > 0 && - enforcedPolicyOptions.minLength > newPassword.length - ) { - return false; - } - - if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) { - return false; - } - - if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) { - return false; - } - - if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) { - return false; - } - - // eslint-disable-next-line - if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) { - return false; - } - - return true; - } - - getResetPasswordPolicyOptions( - policies: Policy[], - orgId: string, - ): [ResetPasswordPolicyOptions, boolean] { - const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions(); - - if (policies == null || orgId == null) { - return [resetPasswordPolicyOptions, false]; - } - - const policy = policies.find( - (p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled, - ); - resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false; - - return [resetPasswordPolicyOptions, policy?.enabled ?? false]; - } - - async upsert(policy: PolicyData): Promise { - await this.activeUserPolicyState.update((policies) => { - policies ??= {}; - policies[policy.id] = policy; - return policies; - }); - } - - async replace(policies: { [id: string]: PolicyData }, userId: UserId): Promise { - await this.stateProvider.setUserState(POLICIES, policies, userId); - } - - /** - * Determines whether an orgUser is exempt from a specific policy because of their role - * Generally orgUsers who can manage policies are exempt from them, but some policies are stricter - */ - private isExemptFromPolicy(policyType: PolicyType, organization: Organization) { - switch (policyType) { - case PolicyType.MaximumVaultTimeout: - // Max Vault Timeout applies to everyone except owners - return organization.isOwner; - case PolicyType.PasswordGenerator: - // password generation policy applies to everyone - return false; - case PolicyType.PersonalOwnership: - // individual vault policy applies to everyone except admins and owners - return organization.isAdmin; - case PolicyType.FreeFamiliesSponsorshipPolicy: - // free Bitwarden families policy applies to everyone - return false; - case PolicyType.RemoveUnlockWithPin: - // free Remove Unlock with PIN policy applies to everyone - return false; - default: - return organization.canManagePolicies; - } - } -} diff --git a/libs/common/src/autofill/services/autofill-settings.service.ts b/libs/common/src/autofill/services/autofill-settings.service.ts index 869ae02c102..3346ef99a58 100644 --- a/libs/common/src/autofill/services/autofill-settings.service.ts +++ b/libs/common/src/autofill/services/autofill-settings.service.ts @@ -1,9 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { map, Observable } from "rxjs"; +import { map, Observable, switchMap } from "rxjs"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { getUserId } from "../../auth/services/account.service"; import { AUTOFILL_SETTINGS_DISK, AUTOFILL_SETTINGS_DISK_LOCAL, @@ -152,6 +154,7 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti constructor( private stateProvider: StateProvider, private policyService: PolicyService, + private accountService: AccountService, ) { this.autofillOnPageLoadState = this.stateProvider.getActive(AUTOFILL_ON_PAGE_LOAD); this.autofillOnPageLoad$ = this.autofillOnPageLoadState.state$.pipe(map((x) => x ?? false)); @@ -169,8 +172,11 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti this.autofillOnPageLoadCalloutIsDismissed$ = this.autofillOnPageLoadCalloutIsDismissedState.state$.pipe(map((x) => x ?? false)); - this.activateAutofillOnPageLoadFromPolicy$ = this.policyService.policyAppliesToActiveUser$( - PolicyType.ActivateAutofill, + this.activateAutofillOnPageLoadFromPolicy$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.ActivateAutofill, userId), + ), ); this.autofillOnPageLoadPolicyToastHasDisplayedState = this.stateProvider.getActive( diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts index 454a748344f..b5e9544b01b 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts @@ -175,7 +175,7 @@ describe("VaultTimeoutSettingsService", () => { "returns $expected when policy is $policy, and user preference is $userPreference", async ({ policy, userPreference, expected }) => { userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); - policyService.getAll$.mockReturnValue( + policyService.policiesByType$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), ); @@ -213,7 +213,7 @@ describe("VaultTimeoutSettingsService", () => { userDecryptionOptionsSubject.next( new UserDecryptionOptions({ hasMasterPassword: false }), ); - policyService.getAll$.mockReturnValue( + policyService.policiesByType$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), ); @@ -257,7 +257,7 @@ describe("VaultTimeoutSettingsService", () => { "when policy is %s, and vault timeout is %s, returns %s", async (policy, vaultTimeout, expected) => { userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); - policyService.getAll$.mockReturnValue( + policyService.policiesByType$.mockReturnValue( of(policy === null ? [] : ([{ data: { minutes: policy } }] as unknown as Policy[])), ); diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts index f29687bd9cf..0716bf0bb93 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts @@ -9,7 +9,6 @@ import { distinctUntilChanged, firstValueFrom, from, - map, shareReplay, switchMap, tap, @@ -24,6 +23,7 @@ import { BiometricStateService, KeyService } from "@bitwarden/key-management"; import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; +import { getFirstPolicy } from "../../../admin-console/services/policy/default-policy.service"; import { AccountService } from "../../../auth/abstractions/account.service"; import { TokenService } from "../../../auth/abstractions/token.service"; import { LogService } from "../../../platform/abstractions/log.service"; @@ -266,8 +266,8 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } return this.policyService - .getAll$(PolicyType.MaximumVaultTimeout, userId) - .pipe(map((policies) => policies[0] ?? null)); + .policiesByType$(PolicyType.MaximumVaultTimeout, userId) + .pipe(getFirstPolicy); } private async getAvailableVaultTimeoutActions(userId?: string): Promise { diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 79d194b87bc..2c6354f5b5a 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -329,7 +329,12 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { private async handlePolicies() { combineLatest([ - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + ), this.organizations$, ]) .pipe(takeUntil(this.destroy$)) diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index b50c7d23337..bcb8b3721ef 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -22,6 +22,7 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { MasterPasswordVerification, MasterPasswordVerificationResponse, @@ -584,7 +585,10 @@ export class LockComponent implements OnInit, OnDestroy { // If we do not have any saved policies, attempt to load them from the service if (this.enforcedMasterPasswordOptions == undefined) { this.enforcedMasterPasswordOptions = await firstValueFrom( - this.policyService.masterPasswordPolicyOptions$(), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + ), ); } 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 f808ff6802a..aecc6dcc330 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 @@ -14,7 +14,6 @@ import { import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms"; import { combineLatest, - firstValueFrom, map, merge, Observable, @@ -212,12 +211,18 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.formDisabled.emit(c === "DISABLED"); }); - // policies - this.disablePersonalVaultExportPolicy$ = this.policyService.policyAppliesToActiveUser$( - PolicyType.DisablePersonalVaultExport, + this.disablePersonalVaultExportPolicy$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisablePersonalVaultExport, userId), + ), ); - this.disablePersonalOwnershipPolicy$ = this.policyService.policyAppliesToActiveUser$( - PolicyType.PersonalOwnership, + + this.disablePersonalOwnershipPolicy$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), ); merge( @@ -227,8 +232,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { .pipe(startWith(0), takeUntil(this.destroy$)) .subscribe(() => this.adjustValidators()); - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - // Wire up the password generation for the password-protected export const account$ = this.accountService.activeAccount$.pipe( pin({ @@ -251,9 +254,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { }); if (this.organizationId) { - this.organizations$ = this.organizationService - .memberOrganizations$(userId) - .pipe(map((orgs) => orgs.filter((org) => org.id == this.organizationId))); + this.organizations$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + 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(); @@ -263,7 +271,10 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.organizations$ = combineLatest({ collections: this.collectionService.decryptedCollections$, - memberOrganizations: this.organizationService.memberOrganizations$(userId), + memberOrganizations: this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.organizationService.memberOrganizations$(userId)), + ), }).pipe( map(({ collections, memberOrganizations }) => { const managedCollectionsOrgIds = new Set( diff --git a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts index 21522bdcb98..2bc8d514873 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts @@ -207,7 +207,7 @@ const providers = { describe("CredentialGeneratorService", () => { beforeEach(async () => { await accountService.switchAccount(SomeUser); - policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable()); + policyService.policiesByType$.mockImplementation(() => new BehaviorSubject([]).asObservable()); i18nService.t.mockImplementation((key: string) => key); apiService.fetch.mockImplementation(() => Promise.resolve(mock())); jest.clearAllMocks(); @@ -567,7 +567,7 @@ describe("CredentialGeneratorService", () => { // awareness; they exercise the logic without being comprehensive it("enforces the active user's policy", async () => { const policy$ = new BehaviorSubject([passwordOverridePolicy]); - policyService.getAll$.mockReturnValue(policy$); + policyService.policiesByType$.mockReturnValue(policy$); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -578,15 +578,22 @@ describe("CredentialGeneratorService", () => { const result = await firstValueFrom(generator.algorithms$(["password"], { account$ })); - expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); + expect(policyService.policiesByType$).toHaveBeenCalledWith( + PolicyType.PasswordGenerator, + SomeUser, + ); expect(result.some((a) => a.id === Generators.password.id)).toBeTruthy(); expect(result.some((a) => a.id === Generators.passphrase.id)).toBeFalsy(); }); it("follows changes to the active user", async () => { const account$ = new BehaviorSubject(accounts[SomeUser]); - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy])); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passwordOverridePolicy]), + ); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passphraseOverridePolicy]), + ); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -603,7 +610,7 @@ describe("CredentialGeneratorService", () => { const [someResult, anotherResult] = results; - expect(policyService.getAll$).toHaveBeenNthCalledWith( + expect(policyService.policiesByType$).toHaveBeenNthCalledWith( 1, PolicyType.PasswordGenerator, SomeUser, @@ -611,7 +618,7 @@ describe("CredentialGeneratorService", () => { expect(someResult.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); expect(someResult.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); - expect(policyService.getAll$).toHaveBeenNthCalledWith( + expect(policyService.policiesByType$).toHaveBeenNthCalledWith( 2, PolicyType.PasswordGenerator, AnotherUser, @@ -621,7 +628,9 @@ describe("CredentialGeneratorService", () => { }); it("reads an arbitrary user's settings", async () => { - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passwordOverridePolicy]), + ); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -633,14 +642,21 @@ describe("CredentialGeneratorService", () => { const result = await firstValueFrom(generator.algorithms$("password", { account$ })); - expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser); + expect(policyService.policiesByType$).toHaveBeenCalledWith( + PolicyType.PasswordGenerator, + AnotherUser, + ); expect(result.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); expect(result.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); }); it("follows changes to the arbitrary user", async () => { - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy])); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passwordOverridePolicy]), + ); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passphraseOverridePolicy]), + ); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -658,17 +674,25 @@ describe("CredentialGeneratorService", () => { sub.unsubscribe(); const [someResult, anotherResult] = results; - expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); + expect(policyService.policiesByType$).toHaveBeenCalledWith( + PolicyType.PasswordGenerator, + SomeUser, + ); expect(someResult.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); expect(someResult.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); - expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser); + expect(policyService.policiesByType$).toHaveBeenCalledWith( + PolicyType.PasswordGenerator, + AnotherUser, + ); expect(anotherResult.some((a: any) => a.id === Generators.passphrase.id)).toBeTruthy(); expect(anotherResult.some((a: any) => a.id === Generators.password.id)).toBeFalsy(); }); it("errors when the arbitrary user's stream errors", async () => { - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passwordOverridePolicy]), + ); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -692,7 +716,9 @@ describe("CredentialGeneratorService", () => { }); it("completes when the arbitrary user's stream completes", async () => { - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passwordOverridePolicy]), + ); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -716,7 +742,9 @@ describe("CredentialGeneratorService", () => { }); it("ignores repeated arbitrary user emissions", async () => { - policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); + policyService.policiesByType$.mockReturnValueOnce( + new BehaviorSubject([passwordOverridePolicy]), + ); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -780,7 +808,7 @@ describe("CredentialGeneratorService", () => { const settings = { foo: "value" }; await stateProvider.setUserState(SettingsKey, settings, SomeUser); const policy$ = new BehaviorSubject([somePolicy]); - policyService.getAll$.mockReturnValue(policy$); + policyService.policiesByType$.mockReturnValue(policy$); const generator = new CredentialGeneratorService( randomizer, policyService, @@ -908,7 +936,7 @@ describe("CredentialGeneratorService", () => { ); const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable(); const policy$ = new BehaviorSubject([somePolicy]); - policyService.getAll$.mockReturnValue(policy$); + policyService.policiesByType$.mockReturnValue(policy$); const result = await firstValueFrom(generator.policy$(SomeConfiguration, { account$ })); @@ -926,7 +954,7 @@ describe("CredentialGeneratorService", () => { const account = new BehaviorSubject(accounts[SomeUser]); const account$ = account.asObservable(); const somePolicySubject = new BehaviorSubject([somePolicy]); - policyService.getAll$.mockReturnValueOnce(somePolicySubject.asObservable()); + policyService.policiesByType$.mockReturnValueOnce(somePolicySubject.asObservable()); const emissions: GeneratorConstraints[] = []; const sub = generator .policy$(SomeConfiguration, { account$ }) @@ -954,7 +982,9 @@ describe("CredentialGeneratorService", () => { const account$ = account.asObservable(); const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable(); const anotherPolicy$ = new BehaviorSubject([]).asObservable(); - policyService.getAll$.mockReturnValueOnce(somePolicy$).mockReturnValueOnce(anotherPolicy$); + policyService.policiesByType$ + .mockReturnValueOnce(somePolicy$) + .mockReturnValueOnce(anotherPolicy$); const emissions: GeneratorConstraints[] = []; const sub = generator .policy$(SomeConfiguration, { account$ }) diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index f8ef11cbbe6..eacc2ca6fc5 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -114,11 +114,13 @@ export class CredentialGeneratorService { const algorithms$ = dependencies.account$.pipe( distinctUntilChanged(), switchMap((account) => { - const policies$ = this.policyService.getAll$(PolicyType.PasswordGenerator, account.id).pipe( - map((p) => new Set(availableAlgorithms(p))), - // complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely - takeUntil(anyComplete(dependencies.account$)), - ); + const policies$ = this.policyService + .policiesByType$(PolicyType.PasswordGenerator, account.id) + .pipe( + map((p) => new Set(availableAlgorithms(p))), + // complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely + takeUntil(anyComplete(dependencies.account$)), + ); return policies$; }), map((available) => { @@ -280,7 +282,7 @@ export class CredentialGeneratorService { switchMap(({ userId, email }) => { // complete policy emissions otherwise `switchMap` holds `policies$` open indefinitely const policies$ = this.policyService - .getAll$(configuration.policy.type, userId) + .policiesByType$(configuration.policy.type, userId) .pipe( mapPolicyToConstraints(configuration.policy, email), takeUntil(anyComplete(dependencies.account$)), diff --git a/libs/tools/generator/core/src/services/default-generator.service.spec.ts b/libs/tools/generator/core/src/services/default-generator.service.spec.ts index 4bef94108f0..eb9642a9417 100644 --- a/libs/tools/generator/core/src/services/default-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/default-generator.service.spec.ts @@ -19,7 +19,7 @@ function mockPolicyService(config?: { state?: BehaviorSubject }) { const service = mock(); const stateValue = config?.state ?? new BehaviorSubject([null]); - service.getAll$.mockReturnValue(stateValue); + service.policiesByType$.mockReturnValue(stateValue); return service; } @@ -103,7 +103,7 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); - expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); + expect(policy.policiesByType$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); }); it("should map the policy using the generation strategy", async () => { @@ -150,7 +150,7 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); await firstValueFrom(service.evaluator$(SomeUser)); - expect(policy.getAll$).toHaveBeenCalledTimes(1); + expect(policy.policiesByType$).toHaveBeenCalledTimes(1); }); it("should cache the password generator policy for each user", async () => { @@ -161,8 +161,16 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); await firstValueFrom(service.evaluator$(AnotherUser)); - expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser); - expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser); + expect(policy.policiesByType$).toHaveBeenNthCalledWith( + 1, + PolicyType.PasswordGenerator, + SomeUser, + ); + expect(policy.policiesByType$).toHaveBeenNthCalledWith( + 2, + PolicyType.PasswordGenerator, + AnotherUser, + ); }); }); diff --git a/libs/tools/generator/core/src/services/default-generator.service.ts b/libs/tools/generator/core/src/services/default-generator.service.ts index 12cfefdd5c6..d4ffa16484b 100644 --- a/libs/tools/generator/core/src/services/default-generator.service.ts +++ b/libs/tools/generator/core/src/services/default-generator.service.ts @@ -51,7 +51,7 @@ export class DefaultGeneratorService implements GeneratorServic } private createEvaluator(userId: UserId) { - const evaluator$ = this.policy.getAll$(this.strategy.policy, userId).pipe( + const evaluator$ = this.policy.policiesByType$(this.strategy.policy, userId).pipe( // create the evaluator from the policies this.strategy.toEvaluator(), ); diff --git a/libs/tools/generator/core/src/services/generator-profile-provider.spec.ts b/libs/tools/generator/core/src/services/generator-profile-provider.spec.ts index 5eafacbef52..aeb1a648a14 100644 --- a/libs/tools/generator/core/src/services/generator-profile-provider.spec.ts +++ b/libs/tools/generator/core/src/services/generator-profile-provider.spec.ts @@ -155,7 +155,7 @@ const NoPolicyProfile: CoreProfileMetadata = { describe("GeneratorProfileProvider", () => { beforeEach(async () => { - policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable()); + policyService.policiesByType$.mockImplementation(() => new BehaviorSubject([]).asObservable()); const encryptor$ = new BehaviorSubject({ userId: SomeUser, encryptor }); encryptorProvider.userEncryptor$.mockReturnValue(encryptor$); jest.clearAllMocks(); @@ -211,7 +211,7 @@ describe("GeneratorProfileProvider", () => { const profileProvider = new GeneratorProfileProvider(dependencyProvider, policyService); const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable(); const policy$ = new BehaviorSubject([somePolicy]); - policyService.getAll$.mockReturnValue(policy$); + policyService.policiesByType$.mockReturnValue(policy$); const result = await firstValueFrom(profileProvider.constraints$(SomeProfile, { account$ })); @@ -223,7 +223,7 @@ describe("GeneratorProfileProvider", () => { const account$ = new BehaviorSubject(accounts[SomeUser]).asObservable(); const expectedPolicy = [somePolicy]; const policy$ = new BehaviorSubject(expectedPolicy); - policyService.getAll$.mockReturnValue(policy$); + policyService.policiesByType$.mockReturnValue(policy$); await firstValueFrom(profileProvider.constraints$(SomeProfile, { account$ })); @@ -284,7 +284,7 @@ describe("GeneratorProfileProvider", () => { const account = new BehaviorSubject(accounts[SomeUser]); const account$ = account.asObservable(); const somePolicySubject = new BehaviorSubject([somePolicy]); - policyService.getAll$.mockReturnValueOnce(somePolicySubject.asObservable()); + policyService.policiesByType$.mockReturnValueOnce(somePolicySubject.asObservable()); const emissions: GeneratorConstraints[] = []; const sub = profileProvider .constraints$(SomeProfile, { account$ }) diff --git a/libs/tools/generator/core/src/services/generator-profile-provider.ts b/libs/tools/generator/core/src/services/generator-profile-provider.ts index 24835e948fd..7088e23d3fe 100644 --- a/libs/tools/generator/core/src/services/generator-profile-provider.ts +++ b/libs/tools/generator/core/src/services/generator-profile-provider.ts @@ -86,7 +86,7 @@ export class GeneratorProfileProvider { ); const policies$ = profile.constraints.type - ? this.policyService.getAll$(profile.constraints.type, account.id) + ? this.policyService.policiesByType$(profile.constraints.type, account.id) : of([]); const context: ProfileContext = { diff --git a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts index 1e558ab352e..65f1669ebd1 100644 --- a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts +++ b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.spec.ts @@ -47,7 +47,7 @@ describe("DefaultGeneratorNavigationService", () => { describe("evaluator$", () => { it("emits a GeneratorNavigationEvaluator", async () => { const policyService = mock({ - getAll$() { + policiesByType$() { return of([]); }, }); @@ -62,7 +62,7 @@ describe("DefaultGeneratorNavigationService", () => { describe("enforcePolicy", () => { it("applies policy", async () => { const policyService = mock({ - getAll$(_type: PolicyType, _user: UserId) { + policiesByType$(_type: PolicyType, _user: UserId) { return of([ new Policy({ id: "" as any, diff --git a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.ts b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.ts index 10781786cfe..7189a7095f0 100644 --- a/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.ts +++ b/libs/tools/generator/extensions/navigation/src/default-generator-navigation.service.ts @@ -41,7 +41,7 @@ export class DefaultGeneratorNavigationService implements GeneratorNavigationSer * @param userId: Identifies the user making the request */ evaluator$(userId: UserId) { - const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe( + const evaluator$ = this.policy.policiesByType$(PolicyType.PasswordGenerator, userId).pipe( reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy), distinctIfShallowMatch(), map((policy) => new GeneratorNavigationEvaluator(policy)), diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts index ce2ff14d4b0..8d24fc4acee 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts @@ -99,7 +99,7 @@ export class SendOptionsComponent implements OnInit { this.accountService.activeAccount$ .pipe( getUserId, - switchMap((userId) => this.policyService.getAll$(PolicyType.SendOptions, userId)), + switchMap((userId) => this.policyService.policiesByType$(PolicyType.SendOptions, userId)), map((policies) => policies?.some((p) => p.data.disableHideEmail)), takeUntilDestroyed(), ) diff --git a/libs/tools/send/send-ui/src/send-form/services/default-send-form-config.service.ts b/libs/tools/send/send-ui/src/send-form/services/default-send-form-config.service.ts index b222aa6cc1a..343fa880795 100644 --- a/libs/tools/send/send-ui/src/send-form/services/default-send-form-config.service.ts +++ b/libs/tools/send/send-ui/src/send-form/services/default-send-form-config.service.ts @@ -1,10 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { inject, Injectable } from "@angular/core"; -import { combineLatest, firstValueFrom, map } from "rxjs"; +import { combineLatest, firstValueFrom, map, switchMap } from "rxjs"; 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"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendId } from "@bitwarden/common/types/guid"; @@ -22,6 +24,7 @@ import { export class DefaultSendFormConfigService implements SendFormConfigService { private policyService: PolicyService = inject(PolicyService); private sendService: SendService = inject(SendService); + private accountService: AccountService = inject(AccountService); async buildConfig( mode: SendFormMode, @@ -40,9 +43,11 @@ export class DefaultSendFormConfigService implements SendFormConfigService { }; } - private areSendsEnabled$ = this.policyService - .policyAppliesToActiveUser$(PolicyType.DisableSend) - .pipe(map((p) => !p)); + private areSendsEnabled$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId)), + map((p) => !p), + ); private getSend(id?: SendId) { if (id == null) { 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 b9add41c222..8e1dde22324 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 @@ -89,9 +89,13 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { ); } - private allowPersonalOwnership$ = this.policyService - .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) - .pipe(map((p) => !p)); + private allowPersonalOwnership$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.PersonalOwnership, userId), + ), + map((p) => !p), + ); private getCipher(userId: UserId, id?: CipherId): Promise { if (id == null) {