mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00: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:
@@ -1,27 +1,23 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
|
||||
/**
|
||||
* Holds information about an account for use in the AccountService
|
||||
* if more information is added, be sure to update the equality method.
|
||||
*/
|
||||
export type AccountInfo = {
|
||||
status: AuthenticationStatus;
|
||||
email: string;
|
||||
name: string | undefined;
|
||||
};
|
||||
|
||||
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 {
|
||||
accounts$: Observable<Record<UserId, AccountInfo>>;
|
||||
activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>;
|
||||
accountLock$: Observable<UserId>;
|
||||
accountLogout$: Observable<UserId>;
|
||||
/**
|
||||
* Updates the `accounts$` observable with the new account data.
|
||||
* @param userId
|
||||
@@ -40,24 +36,6 @@ export abstract class AccountService {
|
||||
* @param email
|
||||
*/
|
||||
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.
|
||||
* @param userId
|
||||
|
||||
@@ -6,6 +6,8 @@ import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
export abstract class AuthService {
|
||||
/** Authentication status for the active user */
|
||||
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.
|
||||
* @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut`
|
||||
|
||||
@@ -8,7 +8,6 @@ import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AccountInfo } from "../abstractions/account.service";
|
||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
|
||||
import {
|
||||
ACCOUNT_ACCOUNTS,
|
||||
@@ -24,9 +23,7 @@ describe("accountService", () => {
|
||||
let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>;
|
||||
let activeAccountIdState: FakeGlobalState<UserId>;
|
||||
const userId = "userId" as UserId;
|
||||
function userInfo(status: AuthenticationStatus): AccountInfo {
|
||||
return { status, email: "email", name: "name" };
|
||||
}
|
||||
const userInfo = { email: "email", name: "name" };
|
||||
|
||||
beforeEach(() => {
|
||||
messagingService = mock();
|
||||
@@ -50,61 +47,49 @@ describe("accountService", () => {
|
||||
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$);
|
||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
||||
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||
activeAccountIdState.stateSubject.next(userId);
|
||||
|
||||
expect(emissions).toEqual([
|
||||
undefined, // initial value
|
||||
{ id: userId, ...userInfo(AuthenticationStatus.Unlocked) },
|
||||
]);
|
||||
});
|
||||
|
||||
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) },
|
||||
{ id: userId, ...userInfo },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should remember the last emitted value", async () => {
|
||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
||||
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||
activeAccountIdState.stateSubject.next(userId);
|
||||
|
||||
expect(await firstValueFrom(sut.activeAccount$)).toEqual({
|
||||
id: userId,
|
||||
...userInfo(AuthenticationStatus.Unlocked),
|
||||
...userInfo,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("accounts$", () => {
|
||||
it("should maintain an accounts cache", async () => {
|
||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) });
|
||||
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||
expect(await firstValueFrom(sut.accounts$)).toEqual({
|
||||
[userId]: userInfo(AuthenticationStatus.Locked),
|
||||
[userId]: userInfo,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("addAccount", () => {
|
||||
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$);
|
||||
|
||||
expect(currentValue).toEqual({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
||||
expect(currentValue).toEqual({ [userId]: userInfo });
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAccountName", () => {
|
||||
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) };
|
||||
const initialState = { [userId]: userInfo };
|
||||
beforeEach(() => {
|
||||
accountsState.stateSubject.next(initialState);
|
||||
});
|
||||
@@ -114,7 +99,7 @@ describe("accountService", () => {
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...userInfo(AuthenticationStatus.Unlocked), name: "new name" },
|
||||
[userId]: { ...userInfo, name: "new name" },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,7 +112,7 @@ describe("accountService", () => {
|
||||
});
|
||||
|
||||
describe("setAccountEmail", () => {
|
||||
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) };
|
||||
const initialState = { [userId]: userInfo };
|
||||
beforeEach(() => {
|
||||
accountsState.stateSubject.next(initialState);
|
||||
});
|
||||
@@ -137,7 +122,7 @@ describe("accountService", () => {
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
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", () => {
|
||||
beforeEach(() => {
|
||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
||||
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||
activeAccountIdState.stateSubject.next(userId);
|
||||
});
|
||||
|
||||
@@ -207,26 +152,4 @@ describe("accountService", () => {
|
||||
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),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
KeyDefinition,
|
||||
} from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
|
||||
export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
|
||||
ACCOUNT_MEMORY,
|
||||
@@ -36,8 +35,6 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
|
||||
accounts$;
|
||||
activeAccount$;
|
||||
accountLock$ = this.lock.asObservable();
|
||||
accountLogout$ = this.logout.asObservable();
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
@@ -74,34 +71,6 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
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> {
|
||||
await this.activeAccountIdState.update(
|
||||
(_, accounts) => {
|
||||
|
||||
@@ -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$", () => {
|
||||
beforeEach(() => {
|
||||
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||
|
||||
@@ -21,6 +21,7 @@ import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
|
||||
export class AuthService implements AuthServiceAbstraction {
|
||||
activeAccountStatus$: Observable<AuthenticationStatus>;
|
||||
authStatuses$: Observable<Record<UserId, AuthenticationStatus>>;
|
||||
|
||||
constructor(
|
||||
protected accountService: AccountService,
|
||||
@@ -36,6 +37,26 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
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> {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { AccountInfo, AccountService } from "../abstractions/account.service";
|
||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
||||
|
||||
@@ -91,7 +90,6 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
const user1AccountInfo: AccountInfo = {
|
||||
name: "Test User 1",
|
||||
email: "test1@email.com",
|
||||
status: AuthenticationStatus.Unlocked,
|
||||
};
|
||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user