mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[PM-25206] Inject service instead of passing as param (#16801)
* Inject service instead of passing as param * [PM-25206] Move locking logic to LockService (#16802) * Move locking logic to lock service * Fix tests * Fix CLI * Fix test * FIx safari build * Update call to lock service * Remove locked callback * Clean up lock service logic * Add tests * Fix cli build * Add extension lock service * Fix cli build * Fix build * Undo ac changes * Undo ac changes * Run prettier * Fix build * Remove duplicate call * [PM-25206] Remove VaultTimeoutService lock logic (#16804) * Move consumers off of vaulttimeoutsettingsservice lock * Fix build * Fix build * Fix build * Fix firefox build * Fix test * Fix ts strict errors * Fix ts strict error * Undo AC changes * Cleanup * Fix * Fix missing service
This commit is contained in:
@@ -1,20 +1,55 @@
|
||||
import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||
import { combineLatest, filter, firstValueFrom, map, timeout } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { StateEventRunnerService } from "@bitwarden/state";
|
||||
|
||||
import { LogoutService } from "../../abstractions";
|
||||
|
||||
export abstract class LockService {
|
||||
/**
|
||||
* Locks all accounts.
|
||||
*/
|
||||
abstract lockAll(): Promise<void>;
|
||||
/**
|
||||
* Performs lock for a user.
|
||||
* @param userId The user id to lock
|
||||
*/
|
||||
abstract lock(userId: UserId): Promise<void>;
|
||||
|
||||
abstract runPlatformOnLockActions(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultLockService implements LockService {
|
||||
constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly vaultTimeoutService: VaultTimeoutService,
|
||||
private readonly biometricService: BiometricsService,
|
||||
private readonly vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private readonly logoutService: LogoutService,
|
||||
private readonly messagingService: MessagingService,
|
||||
private readonly searchService: SearchService,
|
||||
private readonly folderService: FolderService,
|
||||
private readonly masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private readonly stateEventRunnerService: StateEventRunnerService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly systemService: SystemService,
|
||||
private readonly processReloadService: ProcessReloadServiceAbstraction,
|
||||
private readonly logService: LogService,
|
||||
private readonly keyService: KeyService,
|
||||
) {}
|
||||
|
||||
async lockAll() {
|
||||
@@ -36,14 +71,88 @@ export class DefaultLockService implements LockService {
|
||||
);
|
||||
|
||||
for (const otherAccount of accounts.otherAccounts) {
|
||||
await this.vaultTimeoutService.lock(otherAccount);
|
||||
await this.lock(otherAccount);
|
||||
}
|
||||
|
||||
// Do the active account last in case we ever try to route the user on lock
|
||||
// that way this whole operation will be complete before that routing
|
||||
// could take place.
|
||||
if (accounts.activeAccount != null) {
|
||||
await this.vaultTimeoutService.lock(accounts.activeAccount);
|
||||
await this.lock(accounts.activeAccount);
|
||||
}
|
||||
}
|
||||
|
||||
async lock(userId: UserId): Promise<void> {
|
||||
assertNonNullish(userId, "userId", "LockService");
|
||||
|
||||
this.logService.info(`[LockService] Locking user ${userId}`);
|
||||
|
||||
// If user already logged out, then skip locking
|
||||
if (
|
||||
(await firstValueFrom(this.authService.authStatusFor$(userId))) ===
|
||||
AuthenticationStatus.LoggedOut
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If user cannot lock, then logout instead
|
||||
if (!(await this.vaultTimeoutSettingsService.canLock(userId))) {
|
||||
// Logout should perform the same steps
|
||||
await this.logoutService.logout(userId, "vaultTimeout");
|
||||
this.logService.info(`[LockService] User ${userId} cannot lock, logging out instead.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.wipeDecryptedState(userId);
|
||||
await this.waitForLockedStatus(userId);
|
||||
await this.systemService.clearPendingClipboard();
|
||||
await this.runPlatformOnLockActions();
|
||||
|
||||
this.logService.info(`[LockService] Locked user ${userId}`);
|
||||
|
||||
// Subscribers navigate the client to the lock screen based on this lock message.
|
||||
// We need to disable auto-prompting as we are just entering a locked state now.
|
||||
await this.biometricService.setShouldAutopromptNow(false);
|
||||
this.messagingService.send("locked", { userId });
|
||||
|
||||
// Wipe the current process to clear active secrets in memory.
|
||||
await this.processReloadService.startProcessReload();
|
||||
}
|
||||
|
||||
private async wipeDecryptedState(userId: UserId) {
|
||||
// Manually clear state
|
||||
await this.searchService.clearIndex(userId);
|
||||
//! DO NOT REMOVE folderService.clearDecryptedFolderState ! For more information see PM-25660
|
||||
await this.folderService.clearDecryptedFolderState(userId);
|
||||
await this.masterPasswordService.clearMasterKey(userId);
|
||||
await this.cipherService.clearCache(userId);
|
||||
// Clear CLI unlock state
|
||||
await this.keyService.clearStoredUserKey(userId);
|
||||
|
||||
// This will clear ephemeral state such as the user's user key based on the key definition's clear-on
|
||||
await this.stateEventRunnerService.handleEvent("lock", userId);
|
||||
}
|
||||
|
||||
private async waitForLockedStatus(userId: UserId): Promise<void> {
|
||||
// HACK: Start listening for the transition of the locking user from something to the locked state.
|
||||
// This is very much a hack to ensure that the authentication status to retrievable right after
|
||||
// it does its work. Particularly and `"locked"` message. Instead the message should be deprecated
|
||||
// and people should subscribe and react to `authStatusFor$` themselves.
|
||||
await firstValueFrom(
|
||||
this.authService.authStatusFor$(userId).pipe(
|
||||
filter((authStatus) => authStatus === AuthenticationStatus.Locked),
|
||||
timeout({
|
||||
first: 5_000,
|
||||
with: () => {
|
||||
throw new Error("The lock process did not complete in a reasonable amount of time.");
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async runPlatformOnLockActions(): Promise<void> {
|
||||
// No platform specific actions to run for this platform.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { StateEventRunnerService } from "@bitwarden/state";
|
||||
|
||||
import { LogoutService } from "../../abstractions";
|
||||
|
||||
import { DefaultLockService } from "./lock.service";
|
||||
|
||||
@@ -12,10 +27,57 @@ describe("DefaultLockService", () => {
|
||||
const mockUser3 = "user3" as UserId;
|
||||
|
||||
const accountService = mockAccountServiceWith(mockUser1);
|
||||
const vaultTimeoutService = mock<VaultTimeoutService>();
|
||||
const biometricsService = mock<BiometricsService>();
|
||||
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
const logoutService = mock<LogoutService>();
|
||||
const messagingService = mock<MessagingService>();
|
||||
const searchService = mock<SearchService>();
|
||||
const folderService = mock<FolderService>();
|
||||
const masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
const stateEventRunnerService = mock<StateEventRunnerService>();
|
||||
const cipherService = mock<CipherService>();
|
||||
const authService = mock<AuthService>();
|
||||
const systemService = mock<SystemService>();
|
||||
const processReloadService = mock<ProcessReloadServiceAbstraction>();
|
||||
const logService = mock<LogService>();
|
||||
const keyService = mock<KeyService>();
|
||||
const sut = new DefaultLockService(
|
||||
accountService,
|
||||
biometricsService,
|
||||
vaultTimeoutSettingsService,
|
||||
logoutService,
|
||||
messagingService,
|
||||
searchService,
|
||||
folderService,
|
||||
masterPasswordService,
|
||||
stateEventRunnerService,
|
||||
cipherService,
|
||||
authService,
|
||||
systemService,
|
||||
processReloadService,
|
||||
logService,
|
||||
keyService,
|
||||
);
|
||||
|
||||
const sut = new DefaultLockService(accountService, vaultTimeoutService);
|
||||
describe("lockAll", () => {
|
||||
const sut = new DefaultLockService(
|
||||
accountService,
|
||||
biometricsService,
|
||||
vaultTimeoutSettingsService,
|
||||
logoutService,
|
||||
messagingService,
|
||||
searchService,
|
||||
folderService,
|
||||
masterPasswordService,
|
||||
stateEventRunnerService,
|
||||
cipherService,
|
||||
authService,
|
||||
systemService,
|
||||
processReloadService,
|
||||
logService,
|
||||
keyService,
|
||||
);
|
||||
|
||||
it("locks the active account last", async () => {
|
||||
await accountService.addAccount(mockUser2, {
|
||||
name: "name2",
|
||||
@@ -25,19 +87,49 @@ describe("DefaultLockService", () => {
|
||||
|
||||
await accountService.addAccount(mockUser3, {
|
||||
name: "name3",
|
||||
email: "email3@example.com",
|
||||
email: "name3@example.com",
|
||||
emailVerified: false,
|
||||
});
|
||||
|
||||
const lockSpy = jest.spyOn(sut, "lock").mockResolvedValue(undefined);
|
||||
|
||||
await sut.lockAll();
|
||||
|
||||
expect(vaultTimeoutService.lock).toHaveBeenCalledTimes(3);
|
||||
// Non-Active users should be called first
|
||||
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(1, mockUser2);
|
||||
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(2, mockUser3);
|
||||
expect(lockSpy).toHaveBeenNthCalledWith(1, mockUser2);
|
||||
expect(lockSpy).toHaveBeenNthCalledWith(2, mockUser3);
|
||||
|
||||
// Active user should be called last
|
||||
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(3, mockUser1);
|
||||
expect(lockSpy).toHaveBeenNthCalledWith(3, mockUser1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lock", () => {
|
||||
const userId = mockUser1;
|
||||
|
||||
it("returns early if user is already logged out", async () => {
|
||||
authService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.LoggedOut));
|
||||
await sut.lock(userId);
|
||||
// Should return early, not call logoutService.logout
|
||||
expect(logoutService.logout).not.toHaveBeenCalled();
|
||||
expect(stateEventRunnerService.handleEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs out if user cannot lock", async () => {
|
||||
authService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
|
||||
vaultTimeoutSettingsService.canLock.mockResolvedValue(false);
|
||||
await sut.lock(userId);
|
||||
expect(logoutService.logout).toHaveBeenCalledWith(userId, "vaultTimeout");
|
||||
expect(stateEventRunnerService.handleEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("locks user", async () => {
|
||||
authService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Locked));
|
||||
logoutService.logout.mockClear();
|
||||
vaultTimeoutSettingsService.canLock.mockResolvedValue(true);
|
||||
await sut.lock(userId);
|
||||
expect(logoutService.logout).not.toHaveBeenCalled();
|
||||
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", userId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user