1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-7838] [PM-7864] Ensure AuthStatus Changes Before Exiting (#9018)

* Ensure AuthStatus Changes Before Exiting

* Do Not Display Account Without Name Or Email

* Fix Environment Selectors

* Add AccountService.clean to Web
This commit is contained in:
Justin Baur
2024-05-03 16:43:42 -04:00
committed by GitHub
parent b46766affd
commit e4ef7d362e
6 changed files with 182 additions and 85 deletions

View File

@@ -1,5 +1,5 @@
import { MockProxy, any, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { BehaviorSubject, from, of } from "rxjs";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { SearchService } from "../../abstractions/search.service";
@@ -106,6 +106,13 @@ describe("VaultTimeoutService", () => {
// Both are available by default and the specific test can change this per test
availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]);
authService.authStatusFor$.mockImplementation((userId) => {
return from([
accounts[userId]?.authStatus ?? AuthenticationStatus.LoggedOut,
AuthenticationStatus.Locked,
]);
});
authService.getAuthStatus.mockImplementation((userId) => {
return Promise.resolve(accounts[userId]?.authStatus);
});
@@ -387,18 +394,6 @@ describe("VaultTimeoutService", () => {
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1");
});
it("should call messaging service locked message if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
// Currently these pass `undefined` (or what they were given) as the userId back
// but we could change this to give the user that was locked (active) to these methods
// so they don't have to get it their own way, but that is a behavioral change that needs
// to be tested.
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: undefined });
});
it("should call locked callback if no user passed into lock", async () => {
setupLock();
@@ -414,25 +409,31 @@ describe("VaultTimeoutService", () => {
it("should call state event runner with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
const user2 = "user2" as UserId;
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user2");
await vaultTimeoutService.lock(user2);
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", user2);
});
it("should call messaging service locked message with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
const user2 = "user2" as UserId;
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: "user2" });
await vaultTimeoutService.lock(user2);
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: user2 });
});
it("should call locked callback with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
const user2 = "user2" as UserId;
expect(lockedCallback).toHaveBeenCalledWith("user2");
await vaultTimeoutService.lock(user2);
expect(lockedCallback).toHaveBeenCalledWith(user2);
});
});
});

View File

@@ -1,4 +1,4 @@
import { combineLatest, firstValueFrom, switchMap } from "rxjs";
import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
@@ -80,7 +80,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
);
}
async lock(userId?: string): Promise<void> {
async lock(userId?: UserId): Promise<void> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
@@ -94,7 +94,27 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.logOut(userId);
}
const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
const currentUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const lockingUserId = userId ?? currentUserId;
// 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 the `lockedCallback` and `"locked"` message. Instead
// lockedCallback should be deprecated and people should subscribe and react to `authStatusFor$` themselves.
const lockPromise = firstValueFrom(
this.authService.authStatusFor$(lockingUserId).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.");
},
}),
),
);
if (userId == null || userId === currentUserId) {
await this.searchService.clearIndex();
@@ -102,19 +122,21 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.collectionService.clearActiveUserCache();
}
await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId);
await this.masterPasswordService.clearMasterKey(lockingUserId);
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: lockingUserId });
await this.cipherService.clearCache(userId);
await this.cipherService.clearCache(lockingUserId);
await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId);
await this.stateEventRunnerService.handleEvent("lock", lockingUserId);
// FIXME: We should send the userId of the user that was locked, in the case of this method being passed
// undefined then it should give back the currentUserId. Better yet, this method shouldn't take
// an undefined userId at all. All receivers need to be checked for how they handle getting undefined.
this.messagingService.send("locked", { userId: userId });
// HACK: Sit here and wait for the the auth status to transition to `Locked`
// to ensure the message and lockedCallback will get the correct status
// if/when they call it.
await lockPromise;
this.messagingService.send("locked", { userId: lockingUserId });
if (this.lockedCallback != null) {
await this.lockedCallback(userId);
@@ -162,7 +184,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
return diffSeconds >= vaultTimeoutSeconds;
}
private async executeTimeoutAction(userId: string): Promise<void> {
private async executeTimeoutAction(userId: UserId): Promise<void> {
const timeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId),
);