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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user