mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01: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:
parent
b46766affd
commit
e4ef7d362e
@ -1,4 +1,4 @@
|
|||||||
import { Subject, firstValueFrom, map, merge, timeout } from "rxjs";
|
import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PinCryptoServiceAbstraction,
|
PinCryptoServiceAbstraction,
|
||||||
@ -1200,31 +1200,46 @@ export default class MainBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logout(expired: boolean, userId?: UserId) {
|
async logout(expired: boolean, userId?: UserId) {
|
||||||
userId ??= (
|
const activeUserId = await firstValueFrom(
|
||||||
await firstValueFrom(
|
this.accountService.activeAccount$.pipe(
|
||||||
this.accountService.activeAccount$.pipe(
|
map((a) => a?.id),
|
||||||
timeout({
|
timeout({
|
||||||
first: 2000,
|
first: 2000,
|
||||||
with: () => {
|
with: () => {
|
||||||
throw new Error("No active account found to logout");
|
throw new Error("No active account found to logout");
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
)?.id;
|
|
||||||
|
|
||||||
await this.eventUploadService.uploadEvents(userId as UserId);
|
const userBeingLoggedOut = userId ?? activeUserId;
|
||||||
|
|
||||||
|
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
||||||
|
|
||||||
|
// HACK: We shouldn't wait for the authentication status to change but instead subscribe to the
|
||||||
|
// authentication status to do various actions.
|
||||||
|
const logoutPromise = firstValueFrom(
|
||||||
|
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
|
||||||
|
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
|
||||||
|
timeout({
|
||||||
|
first: 5_000,
|
||||||
|
with: () => {
|
||||||
|
throw new Error("The logout process did not complete in a reasonable amount of time.");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.syncService.setLastSync(new Date(0), userId),
|
this.syncService.setLastSync(new Date(0), userBeingLoggedOut),
|
||||||
this.cryptoService.clearKeys(userId),
|
this.cryptoService.clearKeys(userBeingLoggedOut),
|
||||||
this.cipherService.clear(userId),
|
this.cipherService.clear(userBeingLoggedOut),
|
||||||
this.folderService.clear(userId),
|
this.folderService.clear(userBeingLoggedOut),
|
||||||
this.collectionService.clear(userId),
|
this.collectionService.clear(userBeingLoggedOut),
|
||||||
this.passwordGenerationService.clear(userId),
|
this.passwordGenerationService.clear(userBeingLoggedOut),
|
||||||
this.vaultTimeoutSettingsService.clear(userId),
|
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
|
||||||
this.vaultFilterService.clear(),
|
this.vaultFilterService.clear(),
|
||||||
this.biometricStateService.logout(userId),
|
this.biometricStateService.logout(userBeingLoggedOut),
|
||||||
/* We intentionally do not clear:
|
/* We intentionally do not clear:
|
||||||
* - autofillSettingsService
|
* - autofillSettingsService
|
||||||
* - badgeSettingsService
|
* - badgeSettingsService
|
||||||
@ -1235,20 +1250,28 @@ export default class MainBackground {
|
|||||||
//Needs to be checked before state is cleaned
|
//Needs to be checked before state is cleaned
|
||||||
const needStorageReseed = await this.needsStorageReseed();
|
const needStorageReseed = await this.needsStorageReseed();
|
||||||
|
|
||||||
const newActiveUser = await firstValueFrom(
|
const newActiveUser =
|
||||||
this.accountService.nextUpAccount$.pipe(map((a) => a?.id)),
|
userBeingLoggedOut === activeUserId
|
||||||
);
|
? await firstValueFrom(this.accountService.nextUpAccount$.pipe(map((a) => a?.id)))
|
||||||
await this.stateService.clean({ userId: userId });
|
: null;
|
||||||
await this.accountService.clean(userId);
|
|
||||||
|
|
||||||
await this.stateEventRunnerService.handleEvent("logout", userId);
|
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||||
|
await this.accountService.clean(userBeingLoggedOut);
|
||||||
|
|
||||||
|
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
||||||
|
|
||||||
|
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
|
||||||
|
await logoutPromise;
|
||||||
|
|
||||||
|
await this.switchAccount(newActiveUser);
|
||||||
if (newActiveUser != null) {
|
if (newActiveUser != null) {
|
||||||
// we have a new active user, do not continue tearing down application
|
// we have a new active user, do not continue tearing down application
|
||||||
await this.switchAccount(newActiveUser as UserId);
|
|
||||||
this.messagingService.send("switchAccountFinish");
|
this.messagingService.send("switchAccountFinish");
|
||||||
} else {
|
} else {
|
||||||
this.messagingService.send("doneLoggingOut", { expired: expired, userId: userId });
|
this.messagingService.send("doneLoggingOut", {
|
||||||
|
expired: expired,
|
||||||
|
userId: userBeingLoggedOut,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needStorageReseed) {
|
if (needStorageReseed) {
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { firstValueFrom, map, Subject, takeUntil } from "rxjs";
|
import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs";
|
||||||
|
|
||||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@ -566,19 +566,42 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
|
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async logOut(expired: boolean, userId?: string) {
|
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
|
||||||
const userBeingLoggedOut =
|
// passing null-ish values to us.
|
||||||
(userId as UserId) ??
|
private async logOut(expired: boolean, userId: UserId) {
|
||||||
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const userBeingLoggedOut = userId ?? activeUserId;
|
||||||
|
|
||||||
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
|
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
|
||||||
// doesn't attempt to update a user that is being logged out as we will manually
|
// doesn't attempt to update a user that is being logged out as we will manually
|
||||||
// call updateAppMenu when the logout is complete.
|
// call updateAppMenu when the logout is complete.
|
||||||
this.startAccountCleanUp(userBeingLoggedOut);
|
this.startAccountCleanUp(userBeingLoggedOut);
|
||||||
|
|
||||||
let preLogoutActiveUserId;
|
const nextUpAccount =
|
||||||
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
|
activeUserId === userBeingLoggedOut
|
||||||
|
? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts
|
||||||
|
: null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// HACK: We shouldn't wait for authentication status to change here but instead subscribe to the
|
||||||
|
// authentication status to do various actions.
|
||||||
|
const logoutPromise = firstValueFrom(
|
||||||
|
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
|
||||||
|
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
|
||||||
|
timeout({
|
||||||
|
first: 5_000,
|
||||||
|
with: () => {
|
||||||
|
throw new Error(
|
||||||
|
"The logout process did not complete in a reasonable amount of time.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Provide the userId of the user to upload events for
|
// Provide the userId of the user to upload events for
|
||||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
||||||
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
||||||
@ -592,26 +615,33 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
||||||
|
|
||||||
preLogoutActiveUserId = this.activeUserId;
|
|
||||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||||
await this.accountService.clean(userBeingLoggedOut);
|
await this.accountService.clean(userBeingLoggedOut);
|
||||||
|
|
||||||
|
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
|
||||||
|
await logoutPromise;
|
||||||
} finally {
|
} finally {
|
||||||
this.finishAccountCleanUp(userBeingLoggedOut);
|
this.finishAccountCleanUp(userBeingLoggedOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextUpAccount == null) {
|
// We only need to change the display at all if the account being looked at is the one
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// being logged out. If it was a background account, no need to do anything.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
if (userBeingLoggedOut === activeUserId) {
|
||||||
this.router.navigate(["login"]);
|
if (nextUpAccount != null) {
|
||||||
} else if (preLogoutActiveUserId !== nextUpAccount.id) {
|
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
|
||||||
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
|
} else {
|
||||||
|
// We don't have another user to switch to, bring them to the login page so they
|
||||||
|
// can sign into a user.
|
||||||
|
await this.accountService.switchAccount(null);
|
||||||
|
void this.router.navigate(["login"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateAppMenu();
|
await this.updateAppMenu();
|
||||||
|
|
||||||
// This must come last otherwise the logout will prematurely trigger
|
// This must come last otherwise the logout will prematurely trigger
|
||||||
// a process reload before all the state service user data can be cleaned up
|
// a process reload before all the state service user data can be cleaned up
|
||||||
if (userBeingLoggedOut === preLogoutActiveUserId) {
|
if (userBeingLoggedOut === activeUserId) {
|
||||||
this.authService.logOut(async () => {
|
this.authService.logOut(async () => {
|
||||||
if (expired) {
|
if (expired) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
@ -702,7 +732,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
options[1] === "logOut"
|
options[1] === "logOut"
|
||||||
? this.logOut(false, userId)
|
? this.logOut(false, userId as UserId)
|
||||||
: await this.vaultTimeoutService.lock(userId);
|
: await this.vaultTimeoutService.lock(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,11 @@ export class AccountSwitcherComponent {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!active.name && !active.email) {
|
||||||
|
// We need to have this information at minimum to display them.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: active.id,
|
id: active.id,
|
||||||
name: active.name,
|
name: active.name,
|
||||||
|
@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common";
|
|||||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { NavigationEnd, Router } from "@angular/router";
|
import { NavigationEnd, Router } from "@angular/router";
|
||||||
import * as jq from "jquery";
|
import * as jq from "jquery";
|
||||||
import { Subject, firstValueFrom, map, switchMap, takeUntil, timer } from "rxjs";
|
import { Subject, filter, firstValueFrom, map, switchMap, takeUntil, timeout, timer } from "rxjs";
|
||||||
|
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
@ -13,6 +13,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
|||||||
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
@ -136,9 +137,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.router.navigate(["/"]);
|
this.router.navigate(["/"]);
|
||||||
break;
|
break;
|
||||||
case "logout":
|
case "logout":
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.logOut(!!message.expired, message.redirect);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.logOut(!!message.expired, message.redirect);
|
|
||||||
break;
|
break;
|
||||||
case "lockVault":
|
case "lockVault":
|
||||||
await this.vaultTimeoutService.lock();
|
await this.vaultTimeoutService.lock();
|
||||||
@ -266,7 +265,20 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
private async logOut(expired: boolean, redirect = true) {
|
private async logOut(expired: boolean, redirect = true) {
|
||||||
await this.eventUploadService.uploadEvents();
|
await this.eventUploadService.uploadEvents();
|
||||||
const userId = await this.stateService.getUserId();
|
const userId = (await this.stateService.getUserId()) as UserId;
|
||||||
|
|
||||||
|
const logoutPromise = firstValueFrom(
|
||||||
|
this.authService.authStatusFor$(userId).pipe(
|
||||||
|
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
|
||||||
|
timeout({
|
||||||
|
first: 5_000,
|
||||||
|
with: () => {
|
||||||
|
throw new Error("The logout process did not complete in a reasonable amount of time.");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.syncService.setLastSync(new Date(0)),
|
this.syncService.setLastSync(new Date(0)),
|
||||||
this.cryptoService.clearKeys(),
|
this.cryptoService.clearKeys(),
|
||||||
@ -274,11 +286,11 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.folderService.clear(userId),
|
this.folderService.clear(userId),
|
||||||
this.collectionService.clear(userId),
|
this.collectionService.clear(userId),
|
||||||
this.passwordGenerationService.clear(),
|
this.passwordGenerationService.clear(),
|
||||||
this.biometricStateService.logout(userId as UserId),
|
this.biometricStateService.logout(userId),
|
||||||
this.paymentMethodWarningService.clear(),
|
this.paymentMethodWarningService.clear(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
await this.stateEventRunnerService.handleEvent("logout", userId);
|
||||||
|
|
||||||
await this.searchService.clearIndex();
|
await this.searchService.clearIndex();
|
||||||
this.authService.logOut(async () => {
|
this.authService.logOut(async () => {
|
||||||
@ -291,6 +303,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.stateService.clean({ userId: userId });
|
await this.stateService.clean({ userId: userId });
|
||||||
|
await this.accountService.clean(userId);
|
||||||
|
|
||||||
|
await logoutPromise;
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MockProxy, any, mock } from "jest-mock-extended";
|
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 { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
import { SearchService } from "../../abstractions/search.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
|
// Both are available by default and the specific test can change this per test
|
||||||
availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]);
|
availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]);
|
||||||
|
|
||||||
|
authService.authStatusFor$.mockImplementation((userId) => {
|
||||||
|
return from([
|
||||||
|
accounts[userId]?.authStatus ?? AuthenticationStatus.LoggedOut,
|
||||||
|
AuthenticationStatus.Locked,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
authService.getAuthStatus.mockImplementation((userId) => {
|
authService.getAuthStatus.mockImplementation((userId) => {
|
||||||
return Promise.resolve(accounts[userId]?.authStatus);
|
return Promise.resolve(accounts[userId]?.authStatus);
|
||||||
});
|
});
|
||||||
@ -387,18 +394,6 @@ describe("VaultTimeoutService", () => {
|
|||||||
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1");
|
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 () => {
|
it("should call locked callback if no user passed into lock", async () => {
|
||||||
setupLock();
|
setupLock();
|
||||||
|
|
||||||
@ -414,25 +409,31 @@ describe("VaultTimeoutService", () => {
|
|||||||
it("should call state event runner with user passed into lock", async () => {
|
it("should call state event runner with user passed into lock", async () => {
|
||||||
setupLock();
|
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 () => {
|
it("should call messaging service locked message with user passed into lock", async () => {
|
||||||
setupLock();
|
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 () => {
|
it("should call locked callback with user passed into lock", async () => {
|
||||||
setupLock();
|
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 { SearchService } from "../../abstractions/search.service";
|
||||||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.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 });
|
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
|
||||||
if (!authed) {
|
if (!authed) {
|
||||||
return;
|
return;
|
||||||
@ -94,7 +94,27 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
await this.logOut(userId);
|
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) {
|
if (userId == null || userId === currentUserId) {
|
||||||
await this.searchService.clearIndex();
|
await this.searchService.clearIndex();
|
||||||
@ -102,19 +122,21 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
await this.collectionService.clearActiveUserCache();
|
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.setUserKeyAutoUnlock(null, { userId: lockingUserId });
|
||||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
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
|
// HACK: Sit here and wait for the the auth status to transition to `Locked`
|
||||||
// undefined then it should give back the currentUserId. Better yet, this method shouldn't take
|
// to ensure the message and lockedCallback will get the correct status
|
||||||
// an undefined userId at all. All receivers need to be checked for how they handle getting undefined.
|
// if/when they call it.
|
||||||
this.messagingService.send("locked", { userId: userId });
|
await lockPromise;
|
||||||
|
|
||||||
|
this.messagingService.send("locked", { userId: lockingUserId });
|
||||||
|
|
||||||
if (this.lockedCallback != null) {
|
if (this.lockedCallback != null) {
|
||||||
await this.lockedCallback(userId);
|
await this.lockedCallback(userId);
|
||||||
@ -162,7 +184,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
return diffSeconds >= vaultTimeoutSeconds;
|
return diffSeconds >= vaultTimeoutSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeTimeoutAction(userId: string): Promise<void> {
|
private async executeTimeoutAction(userId: UserId): Promise<void> {
|
||||||
const timeoutAction = await firstValueFrom(
|
const timeoutAction = await firstValueFrom(
|
||||||
this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId),
|
this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId),
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user