1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +01:00

[PM-7169][PM-5267] Remove auth status from account info (#8539)

* remove active account unlocked from state service

* Remove status from account service `AccountInfo`

* Fixup lingering usages of status

Fixup missed factories

* Fixup account info usage

* fixup CLI build

* Fixup current account type

* Add helper for all auth statuses to auth service

* Fix tests

* Uncomment mistakenly commented code

* Rework logged out account exclusion tests

* Correct test description

* Avoid getters returning observables

* fixup type
This commit is contained in:
Matt Gibson 2024-04-12 02:25:45 -05:00 committed by GitHub
parent c7ea35280d
commit 8d698d9d84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 200 additions and 304 deletions

View File

@ -6,6 +6,7 @@ import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@ -32,6 +33,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
private location: Location, private location: Location,
private router: Router, private router: Router,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private authService: AuthService,
) {} ) {}
get accountLimit() { get accountLimit() {
@ -42,13 +44,14 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
return this.accountSwitcherService.SPECIAL_ADD_ACCOUNT_ID; return this.accountSwitcherService.SPECIAL_ADD_ACCOUNT_ID;
} }
get availableAccounts$() { readonly availableAccounts$ = this.accountSwitcherService.availableAccounts$;
return this.accountSwitcherService.availableAccounts$; readonly currentAccount$ = this.accountService.activeAccount$.pipe(
} switchMap((a) =>
a == null
get currentAccount$() { ? null
return this.accountService.activeAccount$; : this.authService.activeAccountStatus$.pipe(map((s) => ({ ...a, status: s }))),
} ),
);
async ngOnInit() { async ngOnInit() {
const availableVaultTimeoutActions = await firstValueFrom( const availableVaultTimeoutActions = await firstValueFrom(

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { Observable, combineLatest, switchMap } from "rxjs"; import { Observable, combineLatest, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@ -29,12 +30,14 @@ export class CurrentAccountComponent {
private router: Router, private router: Router,
private location: Location, private location: Location,
private route: ActivatedRoute, private route: ActivatedRoute,
private authService: AuthService,
) { ) {
this.currentAccount$ = combineLatest([ this.currentAccount$ = combineLatest([
this.accountService.activeAccount$, this.accountService.activeAccount$,
this.avatarService.avatarColor$, this.avatarService.avatarColor$,
this.authService.activeAccountStatus$,
]).pipe( ]).pipe(
switchMap(async ([account, avatarColor]) => { switchMap(async ([account, avatarColor, accountStatus]) => {
if (account == null) { if (account == null) {
return null; return null;
} }
@ -42,7 +45,7 @@ export class CurrentAccountComponent {
id: account.id, id: account.id,
name: account.name || account.email, name: account.name || account.email,
email: account.email, email: account.email,
status: account.status, status: accountStatus,
avatarColor, avatarColor,
}; };

View File

@ -1,7 +1,8 @@
import { matches, mock } from "jest-mock-extended"; import { matches, mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs"; import { BehaviorSubject, ReplaySubject, firstValueFrom, of, timeout } from "rxjs";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -12,22 +13,29 @@ import { UserId } from "@bitwarden/common/types/guid";
import { AccountSwitcherService } from "./account-switcher.service"; import { AccountSwitcherService } from "./account-switcher.service";
describe("AccountSwitcherService", () => { describe("AccountSwitcherService", () => {
const accountsSubject = new BehaviorSubject<Record<UserId, AccountInfo>>(null); let accountsSubject: BehaviorSubject<Record<UserId, AccountInfo>>;
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>;
let authStatusSubject: ReplaySubject<Record<UserId, AuthenticationStatus>>;
const accountService = mock<AccountService>(); const accountService = mock<AccountService>();
const avatarService = mock<AvatarService>(); const avatarService = mock<AvatarService>();
const messagingService = mock<MessagingService>(); const messagingService = mock<MessagingService>();
const environmentService = mock<EnvironmentService>(); const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const authService = mock<AuthService>();
let accountSwitcherService: AccountSwitcherService; let accountSwitcherService: AccountSwitcherService;
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
accountsSubject = new BehaviorSubject<Record<UserId, AccountInfo>>(null);
activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
authStatusSubject = new ReplaySubject<Record<UserId, AuthenticationStatus>>(1);
// Use subject to allow for easy updates
accountService.accounts$ = accountsSubject; accountService.accounts$ = accountsSubject;
accountService.activeAccount$ = activeAccountSubject; accountService.activeAccount$ = activeAccountSubject;
authService.authStatuses$ = authStatusSubject;
accountSwitcherService = new AccountSwitcherService( accountSwitcherService = new AccountSwitcherService(
accountService, accountService,
@ -35,48 +43,59 @@ describe("AccountSwitcherService", () => {
messagingService, messagingService,
environmentService, environmentService,
logService, logService,
authService,
); );
}); });
afterEach(() => {
accountsSubject.complete();
activeAccountSubject.complete();
authStatusSubject.complete();
});
describe("availableAccounts$", () => { describe("availableAccounts$", () => {
it("should return all accounts and an add account option when accounts are less than 5", async () => { it("should return all logged in accounts and an add account option when accounts are less than 5", async () => {
const user1AccountInfo: AccountInfo = { const accountInfo: AccountInfo = {
name: "Test User 1", name: "Test User 1",
email: "test1@email.com", email: "test1@email.com",
status: AuthenticationStatus.Unlocked,
}; };
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
accountsSubject.next({ accountsSubject.next({ ["1" as UserId]: accountInfo, ["2" as UserId]: accountInfo });
"1": user1AccountInfo, authStatusSubject.next({
} as Record<UserId, AccountInfo>); ["1" as UserId]: AuthenticationStatus.Unlocked,
["2" as UserId]: AuthenticationStatus.Locked,
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "1" as UserId })); });
activeAccountSubject.next(Object.assign(accountInfo, { id: "1" as UserId }));
const accounts = await firstValueFrom( const accounts = await firstValueFrom(
accountSwitcherService.availableAccounts$.pipe(timeout(20)), accountSwitcherService.availableAccounts$.pipe(timeout(20)),
); );
expect(accounts).toHaveLength(2); expect(accounts).toHaveLength(3);
expect(accounts[0].id).toBe("1"); expect(accounts[0].id).toBe("1");
expect(accounts[0].isActive).toBeTruthy(); expect(accounts[0].isActive).toBeTruthy();
expect(accounts[1].id).toBe("2");
expect(accounts[1].id).toBe("addAccount");
expect(accounts[1].isActive).toBeFalsy(); expect(accounts[1].isActive).toBeFalsy();
expect(accounts[2].id).toBe("addAccount");
expect(accounts[2].isActive).toBeFalsy();
}); });
it.each([5, 6])( it.each([5, 6])(
"should return only accounts if there are %i accounts", "should return only accounts if there are %i accounts",
async (numberOfAccounts) => { async (numberOfAccounts) => {
const seedAccounts: Record<UserId, AccountInfo> = {}; const seedAccounts: Record<UserId, AccountInfo> = {};
const seedStatuses: Record<UserId, AuthenticationStatus> = {};
for (let i = 0; i < numberOfAccounts; i++) { for (let i = 0; i < numberOfAccounts; i++) {
seedAccounts[`${i}` as UserId] = { seedAccounts[`${i}` as UserId] = {
email: `test${i}@email.com`, email: `test${i}@email.com`,
name: "Test User ${i}", name: "Test User ${i}",
status: AuthenticationStatus.Unlocked,
}; };
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
} }
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
accountsSubject.next(seedAccounts); accountsSubject.next(seedAccounts);
authStatusSubject.next(seedStatuses);
activeAccountSubject.next( activeAccountSubject.next(
Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }), Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }),
); );
@ -89,6 +108,26 @@ describe("AccountSwitcherService", () => {
}); });
}, },
); );
it("excludes logged out accounts", async () => {
const user1AccountInfo: AccountInfo = {
name: "Test User 1",
email: "",
};
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
accountsSubject.next({
"1": user1AccountInfo,
} as Record<UserId, AccountInfo>);
const accounts = await firstValueFrom(
accountSwitcherService.availableAccounts$.pipe(timeout(20)),
);
// Add account only
expect(accounts).toHaveLength(1);
expect(accounts[0].id).toBe("addAccount");
});
}); });
describe("selectAccount", () => { describe("selectAccount", () => {

View File

@ -11,6 +11,7 @@ import {
} from "rxjs"; } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -48,25 +49,27 @@ export class AccountSwitcherService {
private messagingService: MessagingService, private messagingService: MessagingService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private logService: LogService, private logService: LogService,
authService: AuthService,
) { ) {
this.availableAccounts$ = combineLatest([ this.availableAccounts$ = combineLatest([
this.accountService.accounts$, accountService.accounts$,
authService.authStatuses$,
this.accountService.activeAccount$, this.accountService.activeAccount$,
]).pipe( ]).pipe(
switchMap(async ([accounts, activeAccount]) => { switchMap(async ([accounts, accountStatuses, activeAccount]) => {
const accountEntries = Object.entries(accounts).filter( const loggedInIds = Object.keys(accounts).filter(
([_, account]) => account.status !== AuthenticationStatus.LoggedOut, (id: UserId) => accountStatuses[id] !== AuthenticationStatus.LoggedOut,
); );
// Accounts shouldn't ever be more than ACCOUNT_LIMIT but just in case do a greater than // Accounts shouldn't ever be more than ACCOUNT_LIMIT but just in case do a greater than
const hasMaxAccounts = accountEntries.length >= this.ACCOUNT_LIMIT; const hasMaxAccounts = loggedInIds.length >= this.ACCOUNT_LIMIT;
const options: AvailableAccount[] = await Promise.all( const options: AvailableAccount[] = await Promise.all(
accountEntries.map(async ([id, account]) => { loggedInIds.map(async (id: UserId) => {
return { return {
name: account.name ?? account.email, name: accounts[id].name ?? accounts[id].email,
email: account.email, email: accounts[id].email,
id: id, id: id,
server: (await this.environmentService.getEnvironment(id))?.getHostname(), server: (await this.environmentService.getEnvironment(id))?.getHostname(),
status: account.status, status: accountStatuses[id],
isActive: id === activeAccount?.id, isActive: id === activeAccount?.id,
avatarColor: await firstValueFrom( avatarColor: await firstValueFrom(
this.avatarService.getUserAvatarColor$(id as UserId), this.avatarService.getUserAvatarColor$(id as UserId),

View File

@ -779,14 +779,14 @@ export default class MainBackground {
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
this.logService, this.logService,
this.accountService, this.authService,
); );
this.eventCollectionService = new EventCollectionService( this.eventCollectionService = new EventCollectionService(
this.cipherService, this.cipherService,
this.stateProvider, this.stateProvider,
this.organizationService, this.organizationService,
this.eventUploadService, this.eventUploadService,
this.accountService, this.authService,
); );
this.totpService = new TotpService(this.cryptoFunctionService, this.logService); this.totpService = new TotpService(this.cryptoFunctionService, this.logService);

View File

@ -5,7 +5,10 @@ import {
organizationServiceFactory, organizationServiceFactory,
OrganizationServiceInitOptions, OrganizationServiceInitOptions,
} from "../../admin-console/background/service-factories/organization-service.factory"; } from "../../admin-console/background/service-factories/organization-service.factory";
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; import {
authServiceFactory,
AuthServiceInitOptions,
} from "../../auth/background/service-factories/auth-service.factory";
import { import {
FactoryOptions, FactoryOptions,
CachedServices, CachedServices,
@ -29,7 +32,8 @@ export type EventCollectionServiceInitOptions = EventCollectionServiceOptions &
CipherServiceInitOptions & CipherServiceInitOptions &
StateServiceInitOptions & StateServiceInitOptions &
OrganizationServiceInitOptions & OrganizationServiceInitOptions &
EventUploadServiceInitOptions; EventUploadServiceInitOptions &
AuthServiceInitOptions;
export function eventCollectionServiceFactory( export function eventCollectionServiceFactory(
cache: { eventCollectionService?: AbstractEventCollectionService } & CachedServices, cache: { eventCollectionService?: AbstractEventCollectionService } & CachedServices,
@ -45,7 +49,7 @@ export function eventCollectionServiceFactory(
await stateProviderFactory(cache, opts), await stateProviderFactory(cache, opts),
await organizationServiceFactory(cache, opts), await organizationServiceFactory(cache, opts),
await eventUploadServiceFactory(cache, opts), await eventUploadServiceFactory(cache, opts),
await accountServiceFactory(cache, opts), await authServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -1,7 +1,10 @@
import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; import {
AuthServiceInitOptions,
authServiceFactory,
} from "../../auth/background/service-factories/auth-service.factory";
import { import {
ApiServiceInitOptions, ApiServiceInitOptions,
apiServiceFactory, apiServiceFactory,
@ -23,7 +26,8 @@ type EventUploadServiceOptions = FactoryOptions;
export type EventUploadServiceInitOptions = EventUploadServiceOptions & export type EventUploadServiceInitOptions = EventUploadServiceOptions &
ApiServiceInitOptions & ApiServiceInitOptions &
StateServiceInitOptions & StateServiceInitOptions &
LogServiceInitOptions; LogServiceInitOptions &
AuthServiceInitOptions;
export function eventUploadServiceFactory( export function eventUploadServiceFactory(
cache: { eventUploadService?: AbstractEventUploadService } & CachedServices, cache: { eventUploadService?: AbstractEventUploadService } & CachedServices,
@ -38,7 +42,7 @@ export function eventUploadServiceFactory(
await apiServiceFactory(cache, opts), await apiServiceFactory(cache, opts),
await stateProviderFactory(cache, opts), await stateProviderFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await accountServiceFactory(cache, opts), await authServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -1,8 +1,10 @@
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { Observable, map } from "rxjs"; import { Observable, combineLatest, map, of, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UserId } from "@bitwarden/common/types/guid";
import { enableAccountSwitching } from "../flags"; import { enableAccountSwitching } from "../flags";
@ -14,14 +16,18 @@ export class HeaderComponent {
@Input() noTheme = false; @Input() noTheme = false;
@Input() hideAccountSwitcher = false; @Input() hideAccountSwitcher = false;
authedAccounts$: Observable<boolean>; authedAccounts$: Observable<boolean>;
constructor(accountService: AccountService) { constructor(accountService: AccountService, authService: AuthService) {
this.authedAccounts$ = accountService.accounts$.pipe( this.authedAccounts$ = accountService.accounts$.pipe(
map((accounts) => { switchMap((accounts) => {
if (!enableAccountSwitching()) { if (!enableAccountSwitching()) {
return false; return of(false);
} }
return Object.values(accounts).some((a) => a.status !== AuthenticationStatus.LoggedOut); return combineLatest(
Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)),
).pipe(
map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)),
);
}), }),
); );
} }

View File

@ -1,9 +1,10 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
import { ToastrService } from "ngx-toastr"; import { ToastrService } from "ngx-toastr";
import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs"; import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
@ -57,8 +58,9 @@ export class AppComponent implements OnInit, OnDestroy {
this.activeUserId = userId; this.activeUserId = userId;
}); });
this.stateService.activeAccountUnlocked$ this.authService.activeAccountStatus$
.pipe( .pipe(
map((status) => status === AuthenticationStatus.Unlocked),
filter((unlocked) => unlocked), filter((unlocked) => unlocked),
concatMap(async () => { concatMap(async () => {
await this.recordActivity(); await this.recordActivity();

View File

@ -678,7 +678,7 @@ export class Main {
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
this.logService, this.logService,
this.accountService, this.authService,
); );
this.eventCollectionService = new EventCollectionService( this.eventCollectionService = new EventCollectionService(
@ -686,7 +686,7 @@ export class Main {
this.stateProvider, this.stateProvider,
this.organizationService, this.organizationService,
this.eventUploadService, this.eventUploadService,
this.accountService, this.authService,
); );
} }

View File

@ -2,7 +2,6 @@ import { Injectable, inject } from "@angular/core";
import { CanActivate, CanActivateFn, Router, UrlTree } from "@angular/router"; import { CanActivate, CanActivateFn, Router, UrlTree } from "@angular/router";
import { Observable, map } from "rxjs"; import { Observable, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@ -43,14 +42,14 @@ const defaultRoutes: UnauthRoutes = {
}; };
function unauthGuard(routes: UnauthRoutes): Observable<boolean | UrlTree> { function unauthGuard(routes: UnauthRoutes): Observable<boolean | UrlTree> {
const accountService = inject(AccountService); const authService = inject(AuthService);
const router = inject(Router); const router = inject(Router);
return accountService.activeAccount$.pipe( return authService.activeAccountStatus$.pipe(
map((accountData) => { map((status) => {
if (accountData == null || accountData.status === AuthenticationStatus.LoggedOut) { if (status == null || status === AuthenticationStatus.LoggedOut) {
return true; return true;
} else if (accountData.status === AuthenticationStatus.Locked) { } else if (status === AuthenticationStatus.Locked) {
return router.createUrlTree([routes.locked]); return router.createUrlTree([routes.locked]);
} else { } else {
return router.createUrlTree([routes.homepage()]); return router.createUrlTree([routes.homepage()]);

View File

@ -756,7 +756,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: EventUploadServiceAbstraction, provide: EventUploadServiceAbstraction,
useClass: EventUploadService, useClass: EventUploadService,
deps: [ApiServiceAbstraction, StateProvider, LogService, AccountServiceAbstraction], deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction],
}), }),
safeProvider({ safeProvider({
provide: EventCollectionServiceAbstraction, provide: EventCollectionServiceAbstraction,
@ -766,7 +766,7 @@ const safeProviders: SafeProvider[] = [
StateProvider, StateProvider,
OrganizationServiceAbstraction, OrganizationServiceAbstraction,
EventUploadServiceAbstraction, EventUploadServiceAbstraction,
AccountServiceAbstraction, AuthServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({

View File

@ -1,6 +1,5 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { import {
FakeAccountService, FakeAccountService,
@ -66,7 +65,6 @@ describe("UserDecryptionOptionsService", () => {
await fakeAccountService.addAccount(givenUser, { await fakeAccountService.addAccount(givenUser, {
name: "Test User 1", name: "Test User 1",
email: "test1@email.com", email: "test1@email.com",
status: AuthenticationStatus.Locked,
}); });
await fakeStateProvider.setUserState( await fakeStateProvider.setUserState(
USER_DECRYPTION_OPTIONS, USER_DECRYPTION_OPTIONS,

View File

@ -1,8 +1,7 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { Observable, ReplaySubject } from "rxjs"; import { ReplaySubject } from "rxjs";
import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
import { AuthenticationStatus } from "../src/auth/enums/authentication-status";
import { UserId } from "../src/types/guid"; import { UserId } from "../src/types/guid";
export function mockAccountServiceWith( export function mockAccountServiceWith(
@ -14,7 +13,6 @@ export function mockAccountServiceWith(
...{ ...{
name: "name", name: "name",
email: "email", email: "email",
status: AuthenticationStatus.Locked,
}, },
}; };
const service = new FakeAccountService({ [userId]: fullInfo }); const service = new FakeAccountService({ [userId]: fullInfo });
@ -34,8 +32,6 @@ export class FakeAccountService implements AccountService {
} }
accounts$ = this.accountsSubject.asObservable(); accounts$ = this.accountsSubject.asObservable();
activeAccount$ = this.activeAccountSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable();
accountLock$: Observable<UserId>;
accountLogout$: Observable<UserId>;
constructor(initialData: Record<UserId, AccountInfo>) { constructor(initialData: Record<UserId, AccountInfo>) {
this.accountsSubject.next(initialData); this.accountsSubject.next(initialData);
@ -57,14 +53,6 @@ export class FakeAccountService implements AccountService {
await this.mock.setAccountEmail(userId, email); await this.mock.setAccountEmail(userId, email);
} }
async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void> {
await this.mock.setAccountStatus(userId, status);
}
async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> {
await this.mock.setMaxAccountStatus(userId, maxStatus);
}
async switchAccount(userId: UserId): Promise<void> { async switchAccount(userId: UserId): Promise<void> {
const next = const next =
userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] }; userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] };

View File

@ -1,27 +1,23 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { AuthenticationStatus } from "../enums/authentication-status";
/** /**
* Holds information about an account for use in the AccountService * Holds information about an account for use in the AccountService
* if more information is added, be sure to update the equality method. * if more information is added, be sure to update the equality method.
*/ */
export type AccountInfo = { export type AccountInfo = {
status: AuthenticationStatus;
email: string; email: string;
name: string | undefined; name: string | undefined;
}; };
export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
return a?.status === b?.status && a?.email === b?.email && a?.name === b?.name; return a?.email === b?.email && a?.name === b?.name;
} }
export abstract class AccountService { export abstract class AccountService {
accounts$: Observable<Record<UserId, AccountInfo>>; accounts$: Observable<Record<UserId, AccountInfo>>;
activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>;
accountLock$: Observable<UserId>;
accountLogout$: Observable<UserId>;
/** /**
* Updates the `accounts$` observable with the new account data. * Updates the `accounts$` observable with the new account data.
* @param userId * @param userId
@ -40,24 +36,6 @@ export abstract class AccountService {
* @param email * @param email
*/ */
abstract setAccountEmail(userId: UserId, email: string): Promise<void>; abstract setAccountEmail(userId: UserId, email: string): Promise<void>;
/**
* Updates the `accounts$` observable with the new account status.
* Also emits the `accountLock$` or `accountLogout$` observable if the status is `Locked` or `LoggedOut` respectively.
* @param userId
* @param status
*/
abstract setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void>;
/**
* Updates the `accounts$` observable with the new account status if the current status is higher than the `maxStatus`.
*
* This method only downgrades status to the maximum value sent in, it will not increase authentication status.
*
* @example An account is transitioning from unlocked to logged out. If callbacks that set the status to locked occur
* after it is updated to logged out, the account will be in the incorrect state.
* @param userId The user id of the account to be updated.
* @param maxStatus The new status of the account.
*/
abstract setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void>;
/** /**
* Updates the `activeAccount$` observable with the new active account. * Updates the `activeAccount$` observable with the new active account.
* @param userId * @param userId

View File

@ -6,6 +6,8 @@ import { AuthenticationStatus } from "../enums/authentication-status";
export abstract class AuthService { export abstract class AuthService {
/** Authentication status for the active user */ /** Authentication status for the active user */
abstract activeAccountStatus$: Observable<AuthenticationStatus>; abstract activeAccountStatus$: Observable<AuthenticationStatus>;
/** Authentication status for all known users */
abstract authStatuses$: Observable<Record<UserId, AuthenticationStatus>>;
/** /**
* Returns an observable authentication status for the given user id. * Returns an observable authentication status for the given user id.
* @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut` * @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut`

View File

@ -8,7 +8,6 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service"; import { MessagingService } from "../../platform/abstractions/messaging.service";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { AccountInfo } from "../abstractions/account.service"; import { AccountInfo } from "../abstractions/account.service";
import { AuthenticationStatus } from "../enums/authentication-status";
import { import {
ACCOUNT_ACCOUNTS, ACCOUNT_ACCOUNTS,
@ -24,9 +23,7 @@ describe("accountService", () => {
let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>; let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>;
let activeAccountIdState: FakeGlobalState<UserId>; let activeAccountIdState: FakeGlobalState<UserId>;
const userId = "userId" as UserId; const userId = "userId" as UserId;
function userInfo(status: AuthenticationStatus): AccountInfo { const userInfo = { email: "email", name: "name" };
return { status, email: "email", name: "name" };
}
beforeEach(() => { beforeEach(() => {
messagingService = mock(); messagingService = mock();
@ -50,61 +47,49 @@ describe("accountService", () => {
expect(emissions).toEqual([undefined]); expect(emissions).toEqual([undefined]);
}); });
it("should emit the active account and status", async () => { it("should emit the active account", async () => {
const emissions = trackEmissions(sut.activeAccount$); const emissions = trackEmissions(sut.activeAccount$);
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); accountsState.stateSubject.next({ [userId]: userInfo });
activeAccountIdState.stateSubject.next(userId); activeAccountIdState.stateSubject.next(userId);
expect(emissions).toEqual([ expect(emissions).toEqual([
undefined, // initial value undefined, // initial value
{ id: userId, ...userInfo(AuthenticationStatus.Unlocked) }, { id: userId, ...userInfo },
]);
});
it("should update the status if the account status changes", async () => {
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
activeAccountIdState.stateSubject.next(userId);
const emissions = trackEmissions(sut.activeAccount$);
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) });
expect(emissions).toEqual([
{ id: userId, ...userInfo(AuthenticationStatus.Unlocked) },
{ id: userId, ...userInfo(AuthenticationStatus.Locked) },
]); ]);
}); });
it("should remember the last emitted value", async () => { it("should remember the last emitted value", async () => {
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); accountsState.stateSubject.next({ [userId]: userInfo });
activeAccountIdState.stateSubject.next(userId); activeAccountIdState.stateSubject.next(userId);
expect(await firstValueFrom(sut.activeAccount$)).toEqual({ expect(await firstValueFrom(sut.activeAccount$)).toEqual({
id: userId, id: userId,
...userInfo(AuthenticationStatus.Unlocked), ...userInfo,
}); });
}); });
}); });
describe("accounts$", () => { describe("accounts$", () => {
it("should maintain an accounts cache", async () => { it("should maintain an accounts cache", async () => {
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); accountsState.stateSubject.next({ [userId]: userInfo });
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) }); accountsState.stateSubject.next({ [userId]: userInfo });
expect(await firstValueFrom(sut.accounts$)).toEqual({ expect(await firstValueFrom(sut.accounts$)).toEqual({
[userId]: userInfo(AuthenticationStatus.Locked), [userId]: userInfo,
}); });
}); });
}); });
describe("addAccount", () => { describe("addAccount", () => {
it("should emit the new account", async () => { it("should emit the new account", async () => {
await sut.addAccount(userId, userInfo(AuthenticationStatus.Unlocked)); await sut.addAccount(userId, userInfo);
const currentValue = await firstValueFrom(sut.accounts$); const currentValue = await firstValueFrom(sut.accounts$);
expect(currentValue).toEqual({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); expect(currentValue).toEqual({ [userId]: userInfo });
}); });
}); });
describe("setAccountName", () => { describe("setAccountName", () => {
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; const initialState = { [userId]: userInfo };
beforeEach(() => { beforeEach(() => {
accountsState.stateSubject.next(initialState); accountsState.stateSubject.next(initialState);
}); });
@ -114,7 +99,7 @@ describe("accountService", () => {
const currentState = await firstValueFrom(accountsState.state$); const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({ expect(currentState).toEqual({
[userId]: { ...userInfo(AuthenticationStatus.Unlocked), name: "new name" }, [userId]: { ...userInfo, name: "new name" },
}); });
}); });
@ -127,7 +112,7 @@ describe("accountService", () => {
}); });
describe("setAccountEmail", () => { describe("setAccountEmail", () => {
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; const initialState = { [userId]: userInfo };
beforeEach(() => { beforeEach(() => {
accountsState.stateSubject.next(initialState); accountsState.stateSubject.next(initialState);
}); });
@ -137,7 +122,7 @@ describe("accountService", () => {
const currentState = await firstValueFrom(accountsState.state$); const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({ expect(currentState).toEqual({
[userId]: { ...userInfo(AuthenticationStatus.Unlocked), email: "new email" }, [userId]: { ...userInfo, email: "new email" },
}); });
}); });
@ -149,49 +134,9 @@ describe("accountService", () => {
}); });
}); });
describe("setAccountStatus", () => {
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) };
beforeEach(() => {
accountsState.stateSubject.next(initialState);
});
it("should update the account", async () => {
await sut.setAccountStatus(userId, AuthenticationStatus.Locked);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: {
...userInfo(AuthenticationStatus.Unlocked),
status: AuthenticationStatus.Locked,
},
});
});
it("should not update if the status is the same", async () => {
await sut.setAccountStatus(userId, AuthenticationStatus.Unlocked);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual(initialState);
});
it("should emit logout if the status is logged out", async () => {
const emissions = trackEmissions(sut.accountLogout$);
await sut.setAccountStatus(userId, AuthenticationStatus.LoggedOut);
expect(emissions).toEqual([userId]);
});
it("should emit lock if the status is locked", async () => {
const emissions = trackEmissions(sut.accountLock$);
await sut.setAccountStatus(userId, AuthenticationStatus.Locked);
expect(emissions).toEqual([userId]);
});
});
describe("switchAccount", () => { describe("switchAccount", () => {
beforeEach(() => { beforeEach(() => {
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); accountsState.stateSubject.next({ [userId]: userInfo });
activeAccountIdState.stateSubject.next(userId); activeAccountIdState.stateSubject.next(userId);
}); });
@ -207,26 +152,4 @@ describe("accountService", () => {
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
}); });
}); });
describe("setMaxAccountStatus", () => {
it("should update the account", async () => {
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: userInfo(AuthenticationStatus.Locked),
});
});
it("should not update if the new max status is higher than the current", async () => {
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.LoggedOut) });
await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: userInfo(AuthenticationStatus.LoggedOut),
});
});
});
}); });

View File

@ -14,7 +14,6 @@ import {
KeyDefinition, KeyDefinition,
} from "../../platform/state"; } from "../../platform/state";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { AuthenticationStatus } from "../enums/authentication-status";
export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>( export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
ACCOUNT_MEMORY, ACCOUNT_MEMORY,
@ -36,8 +35,6 @@ export class AccountServiceImplementation implements InternalAccountService {
accounts$; accounts$;
activeAccount$; activeAccount$;
accountLock$ = this.lock.asObservable();
accountLogout$ = this.logout.asObservable();
constructor( constructor(
private messagingService: MessagingService, private messagingService: MessagingService,
@ -74,34 +71,6 @@ export class AccountServiceImplementation implements InternalAccountService {
await this.setAccountInfo(userId, { email }); await this.setAccountInfo(userId, { email });
} }
async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void> {
await this.setAccountInfo(userId, { status });
if (status === AuthenticationStatus.LoggedOut) {
this.logout.next(userId);
} else if (status === AuthenticationStatus.Locked) {
this.lock.next(userId);
}
}
async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> {
await this.accountsState.update(
(accounts) => {
accounts[userId].status = maxStatus;
return accounts;
},
{
shouldUpdate: (accounts) => {
if (accounts?.[userId] == null) {
throw new Error("Account does not exist");
}
return accounts[userId].status > maxStatus;
},
},
);
}
async switchAccount(userId: UserId): Promise<void> { async switchAccount(userId: UserId): Promise<void> {
await this.activeAccountIdState.update( await this.activeAccountIdState.update(
(_, accounts) => { (_, accounts) => {

View File

@ -122,6 +122,25 @@ describe("AuthService", () => {
}); });
}); });
describe("authStatuses$", () => {
it("requests auth status for all known users", async () => {
const userId2 = Utils.newGuid() as UserId;
await accountService.addAccount(userId2, { email: "email2", name: "name2" });
const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked));
sut.authStatusFor$ = mockFn;
await expect(firstValueFrom(await sut.authStatuses$)).resolves.toEqual({
[userId]: AuthenticationStatus.Locked,
[userId2]: AuthenticationStatus.Locked,
});
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(userId);
expect(mockFn).toHaveBeenCalledWith(userId2);
});
});
describe("authStatusFor$", () => { describe("authStatusFor$", () => {
beforeEach(() => { beforeEach(() => {
tokenService.hasAccessToken$.mockReturnValue(of(true)); tokenService.hasAccessToken$.mockReturnValue(of(true));

View File

@ -21,6 +21,7 @@ import { AuthenticationStatus } from "../enums/authentication-status";
export class AuthService implements AuthServiceAbstraction { export class AuthService implements AuthServiceAbstraction {
activeAccountStatus$: Observable<AuthenticationStatus>; activeAccountStatus$: Observable<AuthenticationStatus>;
authStatuses$: Observable<Record<UserId, AuthenticationStatus>>;
constructor( constructor(
protected accountService: AccountService, protected accountService: AccountService,
@ -36,6 +37,26 @@ export class AuthService implements AuthServiceAbstraction {
return this.authStatusFor$(userId); return this.authStatusFor$(userId);
}), }),
); );
this.authStatuses$ = this.accountService.accounts$.pipe(
map((accounts) => Object.keys(accounts) as UserId[]),
switchMap((entries) =>
combineLatest(
entries.map((userId) =>
this.authStatusFor$(userId).pipe(map((status) => ({ userId, status }))),
),
),
),
map((statuses) => {
return statuses.reduce(
(acc, { userId, status }) => {
acc[userId] = status;
return acc;
},
{} as Record<UserId, AuthenticationStatus>,
);
}),
);
} }
authStatusFor$(userId: UserId): Observable<AuthenticationStatus> { authStatusFor$(userId: UserId): Observable<AuthenticationStatus> {

View File

@ -8,7 +8,6 @@ import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service"; import { I18nService } from "../../platform/abstractions/i18n.service";
import { AccountInfo, AccountService } from "../abstractions/account.service"; import { AccountInfo, AccountService } from "../abstractions/account.service";
import { AuthenticationStatus } from "../enums/authentication-status";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation"; import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
@ -91,7 +90,6 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
const user1AccountInfo: AccountInfo = { const user1AccountInfo: AccountInfo = {
name: "Test User 1", name: "Test User 1",
email: "test1@email.com", email: "test1@email.com",
status: AuthenticationStatus.Unlocked,
}; };
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));

View File

@ -33,10 +33,6 @@ export type InitOptions = {
export abstract class StateService<T extends Account = Account> { export abstract class StateService<T extends Account = Account> {
accounts$: Observable<{ [userId: string]: T }>; accounts$: Observable<{ [userId: string]: T }>;
activeAccount$: Observable<string>; activeAccount$: Observable<string>;
/**
* @deprecated use accountService.activeAccount$ instead
*/
activeAccountUnlocked$: Observable<boolean>;
addAccount: (account: T) => Promise<void>; addAccount: (account: T) => Promise<void>;
setActiveUser: (userId: string) => Promise<void>; setActiveUser: (userId: string) => Promise<void>;

View File

@ -4,7 +4,6 @@ import { firstValueFrom, of, tap } from "rxjs";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { CsprngArray } from "../../types/csprng"; import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
@ -273,15 +272,6 @@ describe("cryptoService", () => {
await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided."); await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
}); });
it("should update the user's lock state", async () => {
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(accountService.mock.setAccountStatus).toHaveBeenCalledWith(
mockUserId,
AuthenticationStatus.Unlocked,
);
});
describe("Pin Key refresh", () => { describe("Pin Key refresh", () => {
let cryptoSvcMakePinKey: jest.SpyInstance; let cryptoSvcMakePinKey: jest.SpyInstance;
const protectedPin = const protectedPin =
@ -353,23 +343,6 @@ describe("cryptoService", () => {
accountService.activeAccount$ = accountService.activeAccountSubject.asObservable(); accountService.activeAccount$ = accountService.activeAccountSubject.asObservable();
}); });
it("sets the maximum account status of the active user id to locked when user id is not specified", async () => {
await cryptoService.clearKeys();
expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith(
mockUserId,
AuthenticationStatus.Locked,
);
});
it("sets the maximum account status of the specified user id to locked when user id is specified", async () => {
const userId = "someOtherUser" as UserId;
await cryptoService.clearKeys(userId);
expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith(
userId,
AuthenticationStatus.Locked,
);
});
describe.each([ describe.each([
USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ENCRYPTED_ORGANIZATION_KEYS,
USER_ENCRYPTED_PROVIDER_KEYS, USER_ENCRYPTED_PROVIDER_KEYS,

View File

@ -7,7 +7,6 @@ import { ProfileProviderOrganizationResponse } from "../../admin-console/models/
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { AccountService } from "../../auth/abstractions/account.service"; import { AccountService } from "../../auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { Utils } from "../../platform/misc/utils"; import { Utils } from "../../platform/misc/utils";
import { CsprngArray } from "../../types/csprng"; import { CsprngArray } from "../../types/csprng";
@ -152,8 +151,6 @@ export class CryptoService implements CryptoServiceAbstraction {
[userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId); [userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId); await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId);
await this.accountService.setAccountStatus(userId, AuthenticationStatus.Unlocked);
await this.storeAdditionalKeys(key, userId); await this.storeAdditionalKeys(key, userId);
} }
@ -256,14 +253,13 @@ export class CryptoService implements CryptoServiceAbstraction {
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key * Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
* @param userId The desired user * @param userId The desired user
*/ */
async clearUserKey(userId: UserId): Promise<void> { private async clearUserKey(userId: UserId): Promise<void> {
if (userId == null) { if (userId == null) {
// nothing to do // nothing to do
return; return;
} }
// Set userId to ensure we have one for the account status update // Set userId to ensure we have one for the account status update
await this.stateProvider.setUserState(USER_KEY, null, userId); await this.stateProvider.setUserState(USER_KEY, null, userId);
await this.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
await this.clearAllStoredUserKeys(userId); await this.clearAllStoredUserKeys(userId);
} }

View File

@ -2,7 +2,6 @@ import { firstValueFrom } from "rxjs";
import { FakeStateProvider, awaitAsync } from "../../../spec"; import { FakeStateProvider, awaitAsync } from "../../../spec";
import { FakeAccountService } from "../../../spec/fake-account-service"; import { FakeAccountService } from "../../../spec/fake-account-service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { CloudRegion, Region } from "../abstractions/environment.service"; import { CloudRegion, Region } from "../abstractions/environment.service";
@ -32,12 +31,10 @@ describe("EnvironmentService", () => {
[testUser]: { [testUser]: {
name: "name", name: "name",
email: "email", email: "email",
status: AuthenticationStatus.Locked,
}, },
[alternateTestUser]: { [alternateTestUser]: {
name: "name", name: "name",
email: "email", email: "email",
status: AuthenticationStatus.Locked,
}, },
}); });
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
@ -50,7 +47,6 @@ describe("EnvironmentService", () => {
id: userId, id: userId,
email: "test@example.com", email: "test@example.com",
name: `Test Name ${userId}`, name: `Test Name ${userId}`,
status: AuthenticationStatus.Unlocked,
}); });
await awaitAsync(); await awaitAsync();
}; };

View File

@ -1,9 +1,8 @@
import { BehaviorSubject, Observable, map } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { Jsonify, JsonValue } from "type-fest"; import { Jsonify, JsonValue } from "type-fest";
import { AccountService } from "../../auth/abstractions/account.service"; import { AccountService } from "../../auth/abstractions/account.service";
import { TokenService } from "../../auth/abstractions/token.service"; import { TokenService } from "../../auth/abstractions/token.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { BiometricKey } from "../../auth/types/biometric-key"; import { BiometricKey } from "../../auth/types/biometric-key";
@ -68,8 +67,6 @@ export class StateService<
protected activeAccountSubject = new BehaviorSubject<string | null>(null); protected activeAccountSubject = new BehaviorSubject<string | null>(null);
activeAccount$ = this.activeAccountSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable();
activeAccountUnlocked$: Observable<boolean>;
private hasBeenInited = false; private hasBeenInited = false;
protected isRecoveredSession = false; protected isRecoveredSession = false;
@ -89,13 +86,7 @@ export class StateService<
protected tokenService: TokenService, protected tokenService: TokenService,
private migrationRunner: MigrationRunner, private migrationRunner: MigrationRunner,
protected useAccountCache: boolean = true, protected useAccountCache: boolean = true,
) { ) {}
this.activeAccountUnlocked$ = this.accountService.activeAccount$.pipe(
map((a) => {
return a?.status === AuthenticationStatus.Unlocked;
}),
);
}
async init(initOptions: InitOptions = {}): Promise<void> { async init(initOptions: InitOptions = {}): Promise<void> {
// Deconstruct and apply defaults // Deconstruct and apply defaults
@ -151,7 +142,6 @@ export class StateService<
await this.accountService.addAccount(state.activeUserId as UserId, { await this.accountService.addAccount(state.activeUserId as UserId, {
name: activeDiskAccount.profile.name, name: activeDiskAccount.profile.name,
email: activeDiskAccount.profile.email, email: activeDiskAccount.profile.email,
status: AuthenticationStatus.LoggedOut,
}); });
} }
await this.accountService.switchAccount(state.activeUserId as UserId); await this.accountService.switchAccount(state.activeUserId as UserId);
@ -177,16 +167,7 @@ export class StateService<
// TODO: Temporary update to avoid routing all account status changes through account service for now. // TODO: Temporary update to avoid routing all account status changes through account service for now.
// The determination of state should be handled by the various services that control those values. // The determination of state should be handled by the various services that control those values.
const token = await this.tokenService.getAccessToken(userId as UserId);
const autoKey = await this.getUserKeyAutoUnlock({ userId: userId });
const accountStatus =
token == null
? AuthenticationStatus.LoggedOut
: autoKey == null
? AuthenticationStatus.Locked
: AuthenticationStatus.Unlocked;
await this.accountService.addAccount(userId as UserId, { await this.accountService.addAccount(userId as UserId, {
status: accountStatus,
name: diskAccount.profile.name, name: diskAccount.profile.name,
email: diskAccount.profile.email, email: diskAccount.profile.email,
}); });
@ -206,7 +187,6 @@ export class StateService<
await this.setLastActive(new Date().getTime(), { userId: account.profile.userId }); await this.setLastActive(new Date().getTime(), { userId: account.profile.userId });
// TODO: Temporary update to avoid routing all account status changes through account service for now. // TODO: Temporary update to avoid routing all account status changes through account service for now.
await this.accountService.addAccount(account.profile.userId as UserId, { await this.accountService.addAccount(account.profile.userId as UserId, {
status: AuthenticationStatus.Locked,
name: account.profile.name, name: account.profile.name,
email: account.profile.email, email: account.profile.email,
}); });
@ -1406,8 +1386,6 @@ export class StateService<
return state; return state;
}); });
// TODO: Invert this logic, we should remove accounts based on logged out emit
await this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.LoggedOut);
} }
// settings persist even on reset, and are not affected by this method // settings persist even on reset, and are not affected by this method

View File

@ -9,7 +9,6 @@ import { Jsonify } from "type-fest";
import { awaitAsync, trackEmissions } from "../../../../spec"; import { awaitAsync, trackEmissions } from "../../../../spec";
import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { AccountInfo } from "../../../auth/abstractions/account.service"; import { AccountInfo } from "../../../auth/abstractions/account.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { StorageServiceProvider } from "../../services/storage-service.provider"; import { StorageServiceProvider } from "../../services/storage-service.provider";
import { StateDefinition } from "../state-definition"; import { StateDefinition } from "../state-definition";
@ -84,7 +83,6 @@ describe("DefaultActiveUserState", () => {
id: userId, id: userId,
email: `test${id}@example.com`, email: `test${id}@example.com`,
name: `Test User ${id}`, name: `Test User ${id}`,
status: AuthenticationStatus.Unlocked,
}); });
await awaitAsync(); await awaitAsync();
}; };

View File

@ -3,7 +3,7 @@ import { firstValueFrom, map, from, zip } from "rxjs";
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
import { EventUploadService } from "../../abstractions/event/event-upload.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service";
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { EventType } from "../../enums"; import { EventType } from "../../enums";
import { EventData } from "../../models/data/event.data"; import { EventData } from "../../models/data/event.data";
@ -18,7 +18,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
private stateProvider: StateProvider, private stateProvider: StateProvider,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private eventUploadService: EventUploadService, private eventUploadService: EventUploadService,
private accountService: AccountService, private authService: AuthService,
) {} ) {}
/** Adds an event to the active user's event collection /** Adds an event to the active user's event collection
@ -71,12 +71,12 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
const cipher$ = from(this.cipherService.get(cipherId)); const cipher$ = from(this.cipherService.get(cipherId));
const [accountInfo, orgIds, cipher] = await firstValueFrom( const [authStatus, orgIds, cipher] = await firstValueFrom(
zip(this.accountService.activeAccount$, orgIds$, cipher$), zip(this.authService.activeAccountStatus$, orgIds$, cipher$),
); );
// The user must be authorized // The user must be authorized
if (accountInfo.status != AuthenticationStatus.Unlocked) { if (authStatus != AuthenticationStatus.Unlocked) {
return false; return false;
} }

View File

@ -2,7 +2,7 @@ import { firstValueFrom, map } from "rxjs";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service"; import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service";
import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { EventData } from "../../models/data/event.data"; import { EventData } from "../../models/data/event.data";
import { EventRequest } from "../../models/request/event.request"; import { EventRequest } from "../../models/request/event.request";
@ -18,7 +18,7 @@ export class EventUploadService implements EventUploadServiceAbstraction {
private apiService: ApiService, private apiService: ApiService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
private logService: LogService, private logService: LogService,
private accountService: AccountService, private authService: AuthService,
) {} ) {}
init(checkOnInterval: boolean) { init(checkOnInterval: boolean) {
@ -43,13 +43,16 @@ export class EventUploadService implements EventUploadServiceAbstraction {
userId = await firstValueFrom(this.stateProvider.activeUserId$); userId = await firstValueFrom(this.stateProvider.activeUserId$);
} }
// Get the auth status from the provided user or the active user if (!userId) {
const userAuth$ = this.accountService.accounts$.pipe( return;
map((accounts) => accounts[userId]?.status === AuthenticationStatus.Unlocked), }
);
const isAuthenticated = await firstValueFrom(userAuth$); const isUnlocked = await firstValueFrom(
if (!isAuthenticated) { this.authService
.authStatusFor$(userId)
.pipe(map((status) => status === AuthenticationStatus.Unlocked)),
);
if (!isUnlocked) {
return; return;
} }

View File

@ -138,7 +138,6 @@ describe("VaultTimeoutService", () => {
if (globalSetups?.userId) { if (globalSetups?.userId) {
accountService.activeAccountSubject.next({ accountService.activeAccountSubject.next({
id: globalSetups.userId as UserId, id: globalSetups.userId as UserId,
status: accounts[globalSetups.userId]?.authStatus,
email: null, email: null,
name: null, name: null,
}); });

View File

@ -8,7 +8,6 @@ import {
awaitAsync, awaitAsync,
mockAccountServiceWith, mockAccountServiceWith,
} from "../../../../spec"; } from "../../../../spec";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
@ -64,7 +63,6 @@ describe("SendService", () => {
id: mockUserId, id: mockUserId,
email: "email", email: "email",
name: "name", name: "name",
status: AuthenticationStatus.Unlocked,
}); });
// Initial encrypted state // Initial encrypted state