mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
Merge branch 'main' into autofill/pm-6546-blurring-of-autofilled-elements-causes-problems-in-blur-event-listeners
This commit is contained in:
@@ -109,6 +109,8 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
|||||||
bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>;
|
bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>;
|
||||||
checkNotificationQueue: ({ sender }: BackgroundSenderParam) => Promise<void>;
|
checkNotificationQueue: ({ sender }: BackgroundSenderParam) => Promise<void>;
|
||||||
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
|
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
|
||||||
|
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
||||||
|
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
||||||
getWebVaultUrlForNotification: () => string;
|
getWebVaultUrlForNotification: () => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
|
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -45,6 +46,7 @@ describe("NotificationBackground", () => {
|
|||||||
const policyService = mock<PolicyService>();
|
const policyService = mock<PolicyService>();
|
||||||
const folderService = mock<FolderService>();
|
const folderService = mock<FolderService>();
|
||||||
const stateService = mock<BrowserStateService>();
|
const stateService = mock<BrowserStateService>();
|
||||||
|
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
||||||
const environmentService = mock<EnvironmentService>();
|
const environmentService = mock<EnvironmentService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ describe("NotificationBackground", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
folderService,
|
folderService,
|
||||||
stateService,
|
stateService,
|
||||||
|
userNotificationSettingsService,
|
||||||
environmentService,
|
environmentService,
|
||||||
logService,
|
logService,
|
||||||
);
|
);
|
||||||
@@ -235,8 +238,8 @@ describe("NotificationBackground", () => {
|
|||||||
let tab: chrome.tabs.Tab;
|
let tab: chrome.tabs.Tab;
|
||||||
let sender: chrome.runtime.MessageSender;
|
let sender: chrome.runtime.MessageSender;
|
||||||
let getAuthStatusSpy: jest.SpyInstance;
|
let getAuthStatusSpy: jest.SpyInstance;
|
||||||
let getDisableAddLoginNotificationSpy: jest.SpyInstance;
|
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
||||||
let getDisableChangedPasswordNotificationSpy: jest.SpyInstance;
|
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||||
let pushAddLoginToQueueSpy: jest.SpyInstance;
|
let pushAddLoginToQueueSpy: jest.SpyInstance;
|
||||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||||
@@ -245,13 +248,13 @@ describe("NotificationBackground", () => {
|
|||||||
tab = createChromeTabMock();
|
tab = createChromeTabMock();
|
||||||
sender = mock<chrome.runtime.MessageSender>({ tab });
|
sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||||
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
|
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
|
||||||
getDisableAddLoginNotificationSpy = jest.spyOn(
|
getEnableAddedLoginPromptSpy = jest.spyOn(
|
||||||
stateService,
|
notificationBackground as any,
|
||||||
"getDisableAddLoginNotification",
|
"getEnableAddedLoginPrompt",
|
||||||
);
|
);
|
||||||
getDisableChangedPasswordNotificationSpy = jest.spyOn(
|
getEnableChangedPasswordPromptSpy = jest.spyOn(
|
||||||
stateService,
|
notificationBackground as any,
|
||||||
"getDisableChangedPasswordNotification",
|
"getEnableChangedPasswordPrompt",
|
||||||
);
|
);
|
||||||
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
|
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
|
||||||
pushChangePasswordToQueueSpy = jest.spyOn(
|
pushChangePasswordToQueueSpy = jest.spyOn(
|
||||||
@@ -272,7 +275,7 @@ describe("NotificationBackground", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getAuthStatusSpy).toHaveBeenCalled();
|
expect(getAuthStatusSpy).toHaveBeenCalled();
|
||||||
expect(getDisableAddLoginNotificationSpy).not.toHaveBeenCalled();
|
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
|
||||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -287,7 +290,7 @@ describe("NotificationBackground", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getAuthStatusSpy).toHaveBeenCalled();
|
expect(getAuthStatusSpy).toHaveBeenCalled();
|
||||||
expect(getDisableAddLoginNotificationSpy).not.toHaveBeenCalled();
|
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
|
||||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -297,13 +300,13 @@ describe("NotificationBackground", () => {
|
|||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
|
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
|
||||||
getDisableAddLoginNotificationSpy.mockReturnValueOnce(true);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
|
||||||
|
|
||||||
sendExtensionRuntimeMessage(message, sender);
|
sendExtensionRuntimeMessage(message, sender);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getAuthStatusSpy).toHaveBeenCalled();
|
expect(getAuthStatusSpy).toHaveBeenCalled();
|
||||||
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
|
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||||
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
|
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
|
||||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
@@ -315,14 +318,14 @@ describe("NotificationBackground", () => {
|
|||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
||||||
getDisableAddLoginNotificationSpy.mockReturnValueOnce(true);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
sendExtensionRuntimeMessage(message, sender);
|
sendExtensionRuntimeMessage(message, sender);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getAuthStatusSpy).toHaveBeenCalled();
|
expect(getAuthStatusSpy).toHaveBeenCalled();
|
||||||
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
|
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
@@ -334,8 +337,8 @@ describe("NotificationBackground", () => {
|
|||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
||||||
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||||
getDisableChangedPasswordNotificationSpy.mockReturnValueOnce(true);
|
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
|
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
|
||||||
]);
|
]);
|
||||||
@@ -344,9 +347,9 @@ describe("NotificationBackground", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getAuthStatusSpy).toHaveBeenCalled();
|
expect(getAuthStatusSpy).toHaveBeenCalled();
|
||||||
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
|
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||||
expect(getDisableChangedPasswordNotificationSpy).toHaveBeenCalled();
|
expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled();
|
||||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -357,7 +360,7 @@ describe("NotificationBackground", () => {
|
|||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
||||||
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
||||||
]);
|
]);
|
||||||
@@ -366,7 +369,7 @@ describe("NotificationBackground", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getAuthStatusSpy).toHaveBeenCalled();
|
expect(getAuthStatusSpy).toHaveBeenCalled();
|
||||||
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
|
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
@@ -376,7 +379,7 @@ describe("NotificationBackground", () => {
|
|||||||
const login = { username: "test", password: "password", url: "https://example.com" };
|
const login = { username: "test", password: "password", url: "https://example.com" };
|
||||||
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
||||||
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
|
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
|
||||||
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||||
|
|
||||||
sendExtensionRuntimeMessage(message, sender);
|
sendExtensionRuntimeMessage(message, sender);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -393,7 +396,7 @@ describe("NotificationBackground", () => {
|
|||||||
} as any;
|
} as any;
|
||||||
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
||||||
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
||||||
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
|
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
|
||||||
]);
|
]);
|
||||||
@@ -409,7 +412,8 @@ describe("NotificationBackground", () => {
|
|||||||
const login = { username: "tEsT", password: "password", url: "https://example.com" };
|
const login = { username: "tEsT", password: "password", url: "https://example.com" };
|
||||||
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
||||||
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
|
||||||
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
|
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
|
||||||
|
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
mock<CipherView>({
|
mock<CipherView>({
|
||||||
id: "cipher-id",
|
id: "cipher-id",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
|||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@@ -57,6 +58,8 @@ export default class NotificationBackground {
|
|||||||
bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab),
|
bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab),
|
||||||
checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab),
|
checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab),
|
||||||
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
|
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
|
||||||
|
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
|
||||||
|
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
|
||||||
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
|
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +70,7 @@ export default class NotificationBackground {
|
|||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
private stateService: BrowserStateService,
|
private stateService: BrowserStateService,
|
||||||
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
@@ -81,6 +85,20 @@ export default class NotificationBackground {
|
|||||||
this.cleanupNotificationQueue();
|
this.cleanupNotificationQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the enableChangedPasswordPrompt setting from the user notification settings service.
|
||||||
|
*/
|
||||||
|
async getEnableChangedPasswordPrompt(): Promise<boolean> {
|
||||||
|
return await firstValueFrom(this.userNotificationSettingsService.enableChangedPasswordPrompt$);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the enableAddedLoginPrompt setting from the user notification settings service.
|
||||||
|
*/
|
||||||
|
async getEnableAddedLoginPrompt(): Promise<boolean> {
|
||||||
|
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the notification queue for any messages that need to be sent to the
|
* Checks the notification queue for any messages that need to be sent to the
|
||||||
* specified tab. If no tab is specified, the current tab will be used.
|
* specified tab. If no tab is specified, the current tab will be used.
|
||||||
@@ -194,9 +212,10 @@ export default class NotificationBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabledAddLogin = await this.stateService.getDisableAddLoginNotification();
|
const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
|
||||||
|
|
||||||
if (authStatus === AuthenticationStatus.Locked) {
|
if (authStatus === AuthenticationStatus.Locked) {
|
||||||
if (!disabledAddLogin) {
|
if (addLoginIsEnabled) {
|
||||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
|
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,14 +226,15 @@ export default class NotificationBackground {
|
|||||||
const usernameMatches = ciphers.filter(
|
const usernameMatches = ciphers.filter(
|
||||||
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
|
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
|
||||||
);
|
);
|
||||||
if (!disabledAddLogin && usernameMatches.length === 0) {
|
if (addLoginIsEnabled && usernameMatches.length === 0) {
|
||||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
|
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabledChangePassword = await this.stateService.getDisableChangedPasswordNotification();
|
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!disabledChangePassword &&
|
changePasswordIsEnabled &&
|
||||||
usernameMatches.length === 1 &&
|
usernameMatches.length === 1 &&
|
||||||
usernameMatches[0].login.password !== loginInfo.password
|
usernameMatches[0].login.password !== loginInfo.password
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CachedServices,
|
||||||
|
factory,
|
||||||
|
FactoryOptions,
|
||||||
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import {
|
||||||
|
stateProviderFactory,
|
||||||
|
StateProviderInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
|
|
||||||
|
export type UserNotificationSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
|
||||||
|
|
||||||
|
export function userNotificationSettingsServiceFactory(
|
||||||
|
cache: { userNotificationSettingsService?: UserNotificationSettingsService } & CachedServices,
|
||||||
|
opts: UserNotificationSettingsServiceInitOptions,
|
||||||
|
): Promise<UserNotificationSettingsService> {
|
||||||
|
return factory(
|
||||||
|
cache,
|
||||||
|
"userNotificationSettingsService",
|
||||||
|
opts,
|
||||||
|
async () => new UserNotificationSettingsService(await stateProviderFactory(cache, opts)),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,11 @@ import { WatchedForm } from "../models/watched-form";
|
|||||||
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
|
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
|
||||||
import { FormData } from "../services/abstractions/autofill.service";
|
import { FormData } from "../services/abstractions/autofill.service";
|
||||||
import { GlobalSettings, UserSettings } from "../types";
|
import { GlobalSettings, UserSettings } from "../types";
|
||||||
import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils";
|
import {
|
||||||
|
getFromLocalStorage,
|
||||||
|
sendExtensionMessage,
|
||||||
|
setupExtensionDisconnectAction,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
interface HTMLElementWithFormOpId extends HTMLElement {
|
interface HTMLElementWithFormOpId extends HTMLElement {
|
||||||
formOpId: string;
|
formOpId: string;
|
||||||
@@ -86,12 +90,11 @@ async function loadNotificationBar() {
|
|||||||
]);
|
]);
|
||||||
const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]);
|
const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]);
|
||||||
|
|
||||||
// These are preferences for whether to show the notification bar based on the user's settings
|
const enableChangedPasswordPrompt = await sendExtensionMessage(
|
||||||
// and they are set in the Settings > Options page in the browser extension.
|
"bgGetEnableChangedPasswordPrompt",
|
||||||
let disabledAddLoginNotification = false;
|
);
|
||||||
let disabledChangedPasswordNotification = false;
|
const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
|
||||||
let showNotificationBar = true;
|
let showNotificationBar = true;
|
||||||
|
|
||||||
// Look up the active user id from storage
|
// Look up the active user id from storage
|
||||||
const activeUserIdKey = "activeUserId";
|
const activeUserIdKey = "activeUserId";
|
||||||
const globalStorageKey = "global";
|
const globalStorageKey = "global";
|
||||||
@@ -121,11 +124,7 @@ async function loadNotificationBar() {
|
|||||||
// Example: '{"bitwarden.com":null}'
|
// Example: '{"bitwarden.com":null}'
|
||||||
const excludedDomainsDict = globalSettings.neverDomains;
|
const excludedDomainsDict = globalSettings.neverDomains;
|
||||||
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) {
|
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) {
|
||||||
// Set local disabled preferences
|
if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
|
||||||
disabledAddLoginNotification = globalSettings.disableAddLoginNotification;
|
|
||||||
disabledChangedPasswordNotification = globalSettings.disableChangedPasswordNotification;
|
|
||||||
|
|
||||||
if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) {
|
|
||||||
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
|
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
|
||||||
handlePageChange();
|
handlePageChange();
|
||||||
}
|
}
|
||||||
@@ -352,9 +351,7 @@ async function loadNotificationBar() {
|
|||||||
// to avoid missing any forms that are added after the page loads
|
// to avoid missing any forms that are added after the page loads
|
||||||
observeDom();
|
observeDom();
|
||||||
|
|
||||||
sendPlatformMessage({
|
void sendExtensionMessage("checkNotificationQueue");
|
||||||
command: "checkNotificationQueue",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a safeguard in case the observer misses a SPA page change.
|
// This is a safeguard in case the observer misses a SPA page change.
|
||||||
@@ -392,10 +389,7 @@ async function loadNotificationBar() {
|
|||||||
*
|
*
|
||||||
* */
|
* */
|
||||||
function collectPageDetails() {
|
function collectPageDetails() {
|
||||||
sendPlatformMessage({
|
void sendExtensionMessage("bgCollectPageDetails", { sender: "notificationBar" });
|
||||||
command: "bgCollectPageDetails",
|
|
||||||
sender: "notificationBar",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// End Page Detail Collection Methods
|
// End Page Detail Collection Methods
|
||||||
@@ -620,10 +614,9 @@ async function loadNotificationBar() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabledBoth = disabledChangedPasswordNotification && disabledAddLoginNotification;
|
// if user has enabled either add login or change password notification, and we have a username and password field
|
||||||
// if user has not disabled both notifications and we have a username and password field,
|
|
||||||
if (
|
if (
|
||||||
!disabledBoth &&
|
(enableChangedPasswordPrompt || enableAddedLoginPrompt) &&
|
||||||
watchedForms[i].usernameEl != null &&
|
watchedForms[i].usernameEl != null &&
|
||||||
watchedForms[i].passwordEl != null
|
watchedForms[i].passwordEl != null
|
||||||
) {
|
) {
|
||||||
@@ -639,10 +632,7 @@ async function loadNotificationBar() {
|
|||||||
const passwordPopulated = login.password != null && login.password !== "";
|
const passwordPopulated = login.password != null && login.password !== "";
|
||||||
if (userNamePopulated && passwordPopulated) {
|
if (userNamePopulated && passwordPopulated) {
|
||||||
processedForm(form);
|
processedForm(form);
|
||||||
sendPlatformMessage({
|
void sendExtensionMessage("bgAddLogin", { login });
|
||||||
command: "bgAddLogin",
|
|
||||||
login,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
} else if (
|
} else if (
|
||||||
userNamePopulated &&
|
userNamePopulated &&
|
||||||
@@ -659,7 +649,7 @@ async function loadNotificationBar() {
|
|||||||
|
|
||||||
// if user has not disabled the password changed notification and we have multiple password fields,
|
// if user has not disabled the password changed notification and we have multiple password fields,
|
||||||
// then check if the user has changed their password
|
// then check if the user has changed their password
|
||||||
if (!disabledChangedPasswordNotification && watchedForms[i].passwordEls != null) {
|
if (enableChangedPasswordPrompt && watchedForms[i].passwordEls != null) {
|
||||||
// Get the values of the password fields
|
// Get the values of the password fields
|
||||||
const passwords: string[] = watchedForms[i].passwordEls
|
const passwords: string[] = watchedForms[i].passwordEls
|
||||||
.filter((el: HTMLInputElement) => el.value != null && el.value !== "")
|
.filter((el: HTMLInputElement) => el.value != null && el.value !== "")
|
||||||
@@ -716,7 +706,7 @@ async function loadNotificationBar() {
|
|||||||
currentPassword: curPass,
|
currentPassword: curPass,
|
||||||
url: document.URL,
|
url: document.URL,
|
||||||
};
|
};
|
||||||
sendPlatformMessage({ command: "bgChangedPassword", data });
|
void sendExtensionMessage("bgChangedPassword", { data });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -954,9 +944,7 @@ async function loadNotificationBar() {
|
|||||||
switch (barType) {
|
switch (barType) {
|
||||||
case "add":
|
case "add":
|
||||||
case "change":
|
case "change":
|
||||||
sendPlatformMessage({
|
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
|
||||||
command: "bgRemoveTabFromNotificationQueue",
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -981,12 +969,6 @@ async function loadNotificationBar() {
|
|||||||
// End Notification Bar Functions (open, close, height adjustment, etc.)
|
// End Notification Bar Functions (open, close, height adjustment, etc.)
|
||||||
|
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
function sendPlatformMessage(msg: any) {
|
|
||||||
// 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
|
|
||||||
chrome.runtime.sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isInIframe() {
|
function isInIframe() {
|
||||||
try {
|
try {
|
||||||
return window.self !== window.top;
|
return window.self !== window.top;
|
||||||
|
|||||||
@@ -166,11 +166,10 @@ describe("AutofillService", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(autofillService, "getOverlayVisibility")
|
.spyOn(autofillService, "getOverlayVisibility")
|
||||||
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
|
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
|
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
|
||||||
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
|
||||||
|
|
||||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
|
||||||
|
|
||||||
[autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => {
|
[autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => {
|
||||||
@@ -195,11 +194,6 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("will inject the bootstrap-autofill-overlay script if the user has the autofill overlay enabled", async () => {
|
it("will inject the bootstrap-autofill-overlay script if the user has the autofill overlay enabled", async () => {
|
||||||
jest
|
|
||||||
.spyOn(autofillService, "getOverlayVisibility")
|
|
||||||
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
|
|
||||||
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
|
||||||
|
|
||||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||||
|
|
||||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||||
@@ -218,7 +212,6 @@ describe("AutofillService", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(autofillService, "getOverlayVisibility")
|
.spyOn(autofillService, "getOverlayVisibility")
|
||||||
.mockResolvedValue(AutofillOverlayVisibility.Off);
|
.mockResolvedValue(AutofillOverlayVisibility.Off);
|
||||||
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
|
||||||
|
|
||||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
|
||||||
|
|
||||||
@@ -235,8 +228,6 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("injects the content-message-handler script if not injecting on page load", async () => {
|
it("injects the content-message-handler script if not injecting on page load", async () => {
|
||||||
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
|
|
||||||
|
|
||||||
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false);
|
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false);
|
||||||
|
|
||||||
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ export type UserSettings = {
|
|||||||
vaultTimeoutAction: VaultTimeoutAction;
|
vaultTimeoutAction: VaultTimeoutAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GlobalSettings = Pick<
|
export type GlobalSettings = Pick<GlobalState, "neverDomains">;
|
||||||
GlobalState,
|
|
||||||
"disableAddLoginNotification" | "disableChangedPasswordNotification" | "neverDomains"
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A HTMLElement (usually a form element) with additional custom properties added by this script
|
* A HTMLElement (usually a form element) with additional custom properties added by this script
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ import {
|
|||||||
BadgeSettingsServiceAbstraction,
|
BadgeSettingsServiceAbstraction,
|
||||||
BadgeSettingsService,
|
BadgeSettingsService,
|
||||||
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
import {
|
||||||
|
UserNotificationSettingsService,
|
||||||
|
UserNotificationSettingsServiceAbstraction,
|
||||||
|
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
@@ -89,6 +93,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
|
|||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
|
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
import {
|
import {
|
||||||
@@ -96,6 +101,7 @@ import {
|
|||||||
DerivedStateProvider,
|
DerivedStateProvider,
|
||||||
GlobalStateProvider,
|
GlobalStateProvider,
|
||||||
SingleUserStateProvider,
|
SingleUserStateProvider,
|
||||||
|
StateEventRunnerService,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
|
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
|
||||||
@@ -103,6 +109,7 @@ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state
|
|||||||
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
||||||
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
||||||
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||||
|
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||||
/* eslint-enable import/no-restricted-paths */
|
/* eslint-enable import/no-restricted-paths */
|
||||||
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||||
@@ -248,6 +255,7 @@ export default class MainBackground {
|
|||||||
searchService: SearchServiceAbstraction;
|
searchService: SearchServiceAbstraction;
|
||||||
notificationsService: NotificationsServiceAbstraction;
|
notificationsService: NotificationsServiceAbstraction;
|
||||||
stateService: StateServiceAbstraction;
|
stateService: StateServiceAbstraction;
|
||||||
|
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
|
||||||
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
||||||
badgeSettingsService: BadgeSettingsServiceAbstraction;
|
badgeSettingsService: BadgeSettingsServiceAbstraction;
|
||||||
systemService: SystemServiceAbstraction;
|
systemService: SystemServiceAbstraction;
|
||||||
@@ -294,11 +302,9 @@ export default class MainBackground {
|
|||||||
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
|
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
|
||||||
vaultSettingsService: VaultSettingsServiceAbstraction;
|
vaultSettingsService: VaultSettingsServiceAbstraction;
|
||||||
biometricStateService: BiometricStateService;
|
biometricStateService: BiometricStateService;
|
||||||
|
stateEventRunnerService: StateEventRunnerService;
|
||||||
ssoLoginService: SsoLoginServiceAbstraction;
|
ssoLoginService: SsoLoginServiceAbstraction;
|
||||||
|
|
||||||
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
|
||||||
backgroundWindow = window;
|
|
||||||
|
|
||||||
onUpdatedRan: boolean;
|
onUpdatedRan: boolean;
|
||||||
onReplacedRan: boolean;
|
onReplacedRan: boolean;
|
||||||
loginToAutoFill: CipherView = null;
|
loginToAutoFill: CipherView = null;
|
||||||
@@ -361,10 +367,24 @@ export default class MainBackground {
|
|||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
)
|
)
|
||||||
: new BackgroundMemoryStorageService();
|
: new BackgroundMemoryStorageService();
|
||||||
this.globalStateProvider = new DefaultGlobalStateProvider(
|
|
||||||
this.memoryStorageForStateProviders,
|
const storageServiceProvider = new StorageServiceProvider(
|
||||||
this.storageService as BrowserLocalStorageService,
|
this.storageService as BrowserLocalStorageService,
|
||||||
|
this.memoryStorageForStateProviders,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
|
||||||
|
|
||||||
|
const stateEventRegistrarService = new StateEventRegistrarService(
|
||||||
|
this.globalStateProvider,
|
||||||
|
storageServiceProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.stateEventRunnerService = new StateEventRunnerService(
|
||||||
|
this.globalStateProvider,
|
||||||
|
storageServiceProvider,
|
||||||
|
);
|
||||||
|
|
||||||
this.encryptService = flagEnabled("multithreadDecryption")
|
this.encryptService = flagEnabled("multithreadDecryption")
|
||||||
? new MultithreadEncryptServiceImplementation(
|
? new MultithreadEncryptServiceImplementation(
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
@@ -374,8 +394,8 @@ export default class MainBackground {
|
|||||||
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
|
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
|
||||||
|
|
||||||
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
||||||
this.memoryStorageForStateProviders,
|
storageServiceProvider,
|
||||||
this.storageService as BrowserLocalStorageService,
|
stateEventRegistrarService,
|
||||||
);
|
);
|
||||||
this.accountService = new AccountServiceImplementation(
|
this.accountService = new AccountServiceImplementation(
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
@@ -384,8 +404,8 @@ export default class MainBackground {
|
|||||||
);
|
);
|
||||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.memoryStorageForStateProviders,
|
storageServiceProvider,
|
||||||
this.storageService as BrowserLocalStorageService,
|
stateEventRegistrarService,
|
||||||
);
|
);
|
||||||
this.derivedStateProvider = new BackgroundDerivedStateProvider(
|
this.derivedStateProvider = new BackgroundDerivedStateProvider(
|
||||||
this.memoryStorageForStateProviders,
|
this.memoryStorageForStateProviders,
|
||||||
@@ -419,6 +439,7 @@ export default class MainBackground {
|
|||||||
this.environmentService,
|
this.environmentService,
|
||||||
migrationRunner,
|
migrationRunner,
|
||||||
);
|
);
|
||||||
|
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
|
||||||
this.platformUtilsService = new BrowserPlatformUtilsService(
|
this.platformUtilsService = new BrowserPlatformUtilsService(
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
(clipboardValue, clearMs) => {
|
(clipboardValue, clearMs) => {
|
||||||
@@ -660,6 +681,7 @@ export default class MainBackground {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
this.authService,
|
this.authService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
|
this.stateEventRunnerService,
|
||||||
lockedCallback,
|
lockedCallback,
|
||||||
logoutCallback,
|
logoutCallback,
|
||||||
);
|
);
|
||||||
@@ -675,7 +697,7 @@ export default class MainBackground {
|
|||||||
this.fileUploadService,
|
this.fileUploadService,
|
||||||
this.sendService,
|
this.sendService,
|
||||||
);
|
);
|
||||||
this.providerService = new ProviderService(this.stateService);
|
this.providerService = new ProviderService(this.stateProvider);
|
||||||
this.syncService = new SyncService(
|
this.syncService = new SyncService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.settingsService,
|
this.settingsService,
|
||||||
@@ -846,6 +868,7 @@ export default class MainBackground {
|
|||||||
this.policyService,
|
this.policyService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
this.userNotificationSettingsService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.logService,
|
this.logService,
|
||||||
);
|
);
|
||||||
@@ -1088,9 +1111,11 @@ export default class MainBackground {
|
|||||||
this.keyConnectorService.clear(),
|
this.keyConnectorService.clear(),
|
||||||
this.vaultFilterService.clear(),
|
this.vaultFilterService.clear(),
|
||||||
this.biometricStateService.logout(userId),
|
this.biometricStateService.logout(userId),
|
||||||
|
this.providerService.save(null, userId),
|
||||||
/* We intentionally do not clear:
|
/* We intentionally do not clear:
|
||||||
* - autofillSettingsService
|
* - autofillSettingsService
|
||||||
* - badgeSettingsService
|
* - badgeSettingsService
|
||||||
|
* - userNotificationSettingsService
|
||||||
*/
|
*/
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1104,6 +1129,8 @@ export default class MainBackground {
|
|||||||
this.searchService.clearIndex();
|
this.searchService.clearIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId);
|
||||||
|
|
||||||
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);
|
await this.switchAccount(newActiveUser as UserId);
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ import {
|
|||||||
platformUtilsServiceFactory,
|
platformUtilsServiceFactory,
|
||||||
PlatformUtilsServiceInitOptions,
|
PlatformUtilsServiceInitOptions,
|
||||||
} from "../../platform/background/service-factories/platform-utils-service.factory";
|
} from "../../platform/background/service-factories/platform-utils-service.factory";
|
||||||
|
import {
|
||||||
|
stateEventRunnerServiceFactory,
|
||||||
|
StateEventRunnerServiceInitOptions,
|
||||||
|
} from "../../platform/background/service-factories/state-event-runner-service.factory";
|
||||||
import {
|
import {
|
||||||
StateServiceInitOptions,
|
StateServiceInitOptions,
|
||||||
stateServiceFactory,
|
stateServiceFactory,
|
||||||
@@ -62,7 +66,8 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
|
|||||||
SearchServiceInitOptions &
|
SearchServiceInitOptions &
|
||||||
StateServiceInitOptions &
|
StateServiceInitOptions &
|
||||||
AuthServiceInitOptions &
|
AuthServiceInitOptions &
|
||||||
VaultTimeoutSettingsServiceInitOptions;
|
VaultTimeoutSettingsServiceInitOptions &
|
||||||
|
StateEventRunnerServiceInitOptions;
|
||||||
|
|
||||||
export function vaultTimeoutServiceFactory(
|
export function vaultTimeoutServiceFactory(
|
||||||
cache: { vaultTimeoutService?: AbstractVaultTimeoutService } & CachedServices,
|
cache: { vaultTimeoutService?: AbstractVaultTimeoutService } & CachedServices,
|
||||||
@@ -84,6 +89,7 @@ export function vaultTimeoutServiceFactory(
|
|||||||
await stateServiceFactory(cache, opts),
|
await stateServiceFactory(cache, opts),
|
||||||
await authServiceFactory(cache, opts),
|
await authServiceFactory(cache, opts),
|
||||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
||||||
|
await stateEventRunnerServiceFactory(cache, opts),
|
||||||
opts.vaultTimeoutServiceOptions.lockedCallback,
|
opts.vaultTimeoutServiceOptions.lockedCallback,
|
||||||
opts.vaultTimeoutServiceOptions.loggedOutCallback,
|
opts.vaultTimeoutServiceOptions.loggedOutCallback,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -108,6 +108,7 @@
|
|||||||
},
|
},
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"content/fido2/page-script.js",
|
"content/fido2/page-script.js",
|
||||||
|
"content/lp-suppress-import-download.js",
|
||||||
"notification/bar.html",
|
"notification/bar.html",
|
||||||
"images/icon38.png",
|
"images/icon38.png",
|
||||||
"images/icon38_locked.png",
|
"images/icon38_locked.png",
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
"matches": ["http://*/*", "https://*/*", "file:///*"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"all_frames": false,
|
||||||
|
"js": ["content/lp-fileless-importer.js"],
|
||||||
|
"matches": ["https://lastpass.com/export.php"],
|
||||||
|
"run_at": "document_start"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"css": ["content/autofill.css"],
|
"css": ["content/autofill.css"],
|
||||||
|
|||||||
@@ -9,18 +9,20 @@ import {
|
|||||||
|
|
||||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
import {
|
import {
|
||||||
DiskStorageServiceInitOptions,
|
StateEventRegistrarServiceInitOptions,
|
||||||
MemoryStorageServiceInitOptions,
|
stateEventRegistrarServiceFactory,
|
||||||
observableDiskStorageServiceFactory,
|
} from "./state-event-registrar-service.factory";
|
||||||
observableMemoryStorageServiceFactory,
|
import {
|
||||||
} from "./storage-service.factory";
|
StorageServiceProviderInitOptions,
|
||||||
|
storageServiceProviderFactory,
|
||||||
|
} from "./storage-service-provider.factory";
|
||||||
|
|
||||||
type ActiveUserStateProviderFactory = FactoryOptions;
|
type ActiveUserStateProviderFactory = FactoryOptions;
|
||||||
|
|
||||||
export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory &
|
export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory &
|
||||||
AccountServiceInitOptions &
|
AccountServiceInitOptions &
|
||||||
MemoryStorageServiceInitOptions &
|
StorageServiceProviderInitOptions &
|
||||||
DiskStorageServiceInitOptions;
|
StateEventRegistrarServiceInitOptions;
|
||||||
|
|
||||||
export async function activeUserStateProviderFactory(
|
export async function activeUserStateProviderFactory(
|
||||||
cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices,
|
cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices,
|
||||||
@@ -33,8 +35,8 @@ export async function activeUserStateProviderFactory(
|
|||||||
async () =>
|
async () =>
|
||||||
new DefaultActiveUserStateProvider(
|
new DefaultActiveUserStateProvider(
|
||||||
await accountServiceFactory(cache, opts),
|
await accountServiceFactory(cache, opts),
|
||||||
await observableMemoryStorageServiceFactory(cache, opts),
|
await storageServiceProviderFactory(cache, opts),
|
||||||
await observableDiskStorageServiceFactory(cache, opts),
|
await stateEventRegistrarServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,14 @@ import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/imp
|
|||||||
|
|
||||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
import {
|
import {
|
||||||
DiskStorageServiceInitOptions,
|
StorageServiceProviderInitOptions,
|
||||||
MemoryStorageServiceInitOptions,
|
storageServiceProviderFactory,
|
||||||
observableDiskStorageServiceFactory,
|
} from "./storage-service-provider.factory";
|
||||||
observableMemoryStorageServiceFactory,
|
|
||||||
} from "./storage-service.factory";
|
|
||||||
|
|
||||||
type GlobalStateProviderFactoryOptions = FactoryOptions;
|
type GlobalStateProviderFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type GlobalStateProviderInitOptions = GlobalStateProviderFactoryOptions &
|
export type GlobalStateProviderInitOptions = GlobalStateProviderFactoryOptions &
|
||||||
MemoryStorageServiceInitOptions &
|
StorageServiceProviderInitOptions;
|
||||||
DiskStorageServiceInitOptions;
|
|
||||||
|
|
||||||
export async function globalStateProviderFactory(
|
export async function globalStateProviderFactory(
|
||||||
cache: { globalStateProvider?: GlobalStateProvider } & CachedServices,
|
cache: { globalStateProvider?: GlobalStateProvider } & CachedServices,
|
||||||
@@ -24,10 +21,6 @@ export async function globalStateProviderFactory(
|
|||||||
cache,
|
cache,
|
||||||
"globalStateProvider",
|
"globalStateProvider",
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () => new DefaultGlobalStateProvider(await storageServiceProviderFactory(cache, opts)),
|
||||||
new DefaultGlobalStateProvider(
|
|
||||||
await observableMemoryStorageServiceFactory(cache, opts),
|
|
||||||
await observableDiskStorageServiceFactory(cache, opts),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,19 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state
|
|||||||
|
|
||||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
import {
|
import {
|
||||||
DiskStorageServiceInitOptions,
|
StateEventRegistrarServiceInitOptions,
|
||||||
MemoryStorageServiceInitOptions,
|
stateEventRegistrarServiceFactory,
|
||||||
observableDiskStorageServiceFactory,
|
} from "./state-event-registrar-service.factory";
|
||||||
observableMemoryStorageServiceFactory,
|
import {
|
||||||
} from "./storage-service.factory";
|
StorageServiceProviderInitOptions,
|
||||||
|
storageServiceProviderFactory,
|
||||||
|
} from "./storage-service-provider.factory";
|
||||||
|
|
||||||
type SingleUserStateProviderFactoryOptions = FactoryOptions;
|
type SingleUserStateProviderFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type SingleUserStateProviderInitOptions = SingleUserStateProviderFactoryOptions &
|
export type SingleUserStateProviderInitOptions = SingleUserStateProviderFactoryOptions &
|
||||||
MemoryStorageServiceInitOptions &
|
StorageServiceProviderInitOptions &
|
||||||
DiskStorageServiceInitOptions;
|
StateEventRegistrarServiceInitOptions;
|
||||||
|
|
||||||
export async function singleUserStateProviderFactory(
|
export async function singleUserStateProviderFactory(
|
||||||
cache: { singleUserStateProvider?: SingleUserStateProvider } & CachedServices,
|
cache: { singleUserStateProvider?: SingleUserStateProvider } & CachedServices,
|
||||||
@@ -26,8 +28,8 @@ export async function singleUserStateProviderFactory(
|
|||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
new DefaultSingleUserStateProvider(
|
new DefaultSingleUserStateProvider(
|
||||||
await observableMemoryStorageServiceFactory(cache, opts),
|
await storageServiceProviderFactory(cache, opts),
|
||||||
await observableDiskStorageServiceFactory(cache, opts),
|
await stateEventRegistrarServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
|
import {
|
||||||
|
GlobalStateProviderInitOptions,
|
||||||
|
globalStateProviderFactory,
|
||||||
|
} from "./global-state-provider.factory";
|
||||||
|
import {
|
||||||
|
StorageServiceProviderInitOptions,
|
||||||
|
storageServiceProviderFactory,
|
||||||
|
} from "./storage-service-provider.factory";
|
||||||
|
|
||||||
|
type StateEventRunnerServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
|
export type StateEventRunnerServiceInitOptions = StateEventRunnerServiceFactoryOptions &
|
||||||
|
GlobalStateProviderInitOptions &
|
||||||
|
StorageServiceProviderInitOptions;
|
||||||
|
|
||||||
|
export function stateEventRunnerServiceFactory(
|
||||||
|
cache: { stateEventRunnerService?: StateEventRunnerService } & CachedServices,
|
||||||
|
opts: StateEventRunnerServiceInitOptions,
|
||||||
|
): Promise<StateEventRunnerService> {
|
||||||
|
return factory(
|
||||||
|
cache,
|
||||||
|
"stateEventRunnerService",
|
||||||
|
opts,
|
||||||
|
async () =>
|
||||||
|
new StateEventRunnerService(
|
||||||
|
await globalStateProviderFactory(cache, opts),
|
||||||
|
await storageServiceProviderFactory(cache, opts),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -320,6 +320,60 @@ describe("BrowserApi", () => {
|
|||||||
},
|
},
|
||||||
files: [injectDetails.file],
|
files: [injectDetails.file],
|
||||||
injectImmediately: true,
|
injectImmediately: true,
|
||||||
|
world: "ISOLATED",
|
||||||
|
});
|
||||||
|
expect(result).toEqual(executeScriptResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects the script into a specified frameId when the extension is built for manifest v3", async () => {
|
||||||
|
const tabId = 1;
|
||||||
|
const frameId = 2;
|
||||||
|
const injectDetails = mock<chrome.tabs.InjectDetails>({
|
||||||
|
file: "file.js",
|
||||||
|
allFrames: true,
|
||||||
|
runAt: "document_start",
|
||||||
|
frameId,
|
||||||
|
});
|
||||||
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||||
|
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
|
||||||
|
|
||||||
|
await BrowserApi.executeScriptInTab(tabId, injectDetails);
|
||||||
|
|
||||||
|
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
|
||||||
|
target: {
|
||||||
|
tabId: tabId,
|
||||||
|
allFrames: injectDetails.allFrames,
|
||||||
|
frameIds: [frameId],
|
||||||
|
},
|
||||||
|
files: [injectDetails.file],
|
||||||
|
injectImmediately: true,
|
||||||
|
world: "ISOLATED",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects the script into the MAIN world context when injecting a script for manifest v3", async () => {
|
||||||
|
const tabId = 1;
|
||||||
|
const injectDetails = mock<chrome.tabs.InjectDetails>({
|
||||||
|
file: null,
|
||||||
|
allFrames: true,
|
||||||
|
runAt: "document_start",
|
||||||
|
frameId: null,
|
||||||
|
});
|
||||||
|
const scriptingApiDetails = { world: "MAIN" as chrome.scripting.ExecutionWorld };
|
||||||
|
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
|
||||||
|
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
|
||||||
|
|
||||||
|
const result = await BrowserApi.executeScriptInTab(tabId, injectDetails, scriptingApiDetails);
|
||||||
|
|
||||||
|
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
|
||||||
|
target: {
|
||||||
|
tabId: tabId,
|
||||||
|
allFrames: injectDetails.allFrames,
|
||||||
|
frameIds: null,
|
||||||
|
},
|
||||||
|
files: null,
|
||||||
|
injectImmediately: true,
|
||||||
|
world: "MAIN",
|
||||||
});
|
});
|
||||||
expect(result).toEqual(executeScriptResult);
|
expect(result).toEqual(executeScriptResult);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -475,12 +475,19 @@ export class BrowserApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension API helper method used to execute a script in a tab.
|
* Extension API helper method used to execute a script in a tab.
|
||||||
|
*
|
||||||
* @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript
|
* @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript
|
||||||
* @param {number} tabId
|
* @param tabId - The id of the tab to execute the script in.
|
||||||
* @param {chrome.tabs.InjectDetails} details
|
* @param details {@link "InjectDetails" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/extensionTypes/InjectDetails}
|
||||||
* @returns {Promise<unknown>}
|
* @param scriptingApiDetails {@link "ExecutionWorld" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld}
|
||||||
*/
|
*/
|
||||||
static executeScriptInTab(tabId: number, details: chrome.tabs.InjectDetails) {
|
static executeScriptInTab(
|
||||||
|
tabId: number,
|
||||||
|
details: chrome.tabs.InjectDetails,
|
||||||
|
scriptingApiDetails?: {
|
||||||
|
world: chrome.scripting.ExecutionWorld;
|
||||||
|
},
|
||||||
|
): Promise<unknown> {
|
||||||
if (BrowserApi.manifestVersion === 3) {
|
if (BrowserApi.manifestVersion === 3) {
|
||||||
return chrome.scripting.executeScript({
|
return chrome.scripting.executeScript({
|
||||||
target: {
|
target: {
|
||||||
@@ -490,6 +497,7 @@ export class BrowserApi {
|
|||||||
},
|
},
|
||||||
files: details.file ? [details.file] : null,
|
files: details.file ? [details.file] : null,
|
||||||
injectImmediately: details.runAt === "document_start",
|
injectImmediately: details.runAt === "document_start",
|
||||||
|
world: scriptingApiDetails?.world || "ISOLATED",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
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";
|
||||||
@@ -25,7 +24,6 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
|
||||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
@@ -42,6 +40,10 @@ import {
|
|||||||
AutofillSettingsService,
|
AutofillSettingsService,
|
||||||
AutofillSettingsServiceAbstraction,
|
AutofillSettingsServiceAbstraction,
|
||||||
} from "@bitwarden/common/autofill/services/autofill-settings.service";
|
} from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
|
import {
|
||||||
|
UserNotificationSettingsService,
|
||||||
|
UserNotificationSettingsServiceAbstraction,
|
||||||
|
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
@@ -207,7 +209,6 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
},
|
},
|
||||||
deps: [LogServiceAbstraction, I18nServiceAbstraction],
|
deps: [LogServiceAbstraction, I18nServiceAbstraction],
|
||||||
},
|
},
|
||||||
{ provide: AuditService, useFactory: getBgService<AuditService>("auditService"), deps: [] },
|
|
||||||
{
|
{
|
||||||
provide: CipherFileUploadService,
|
provide: CipherFileUploadService,
|
||||||
useFactory: getBgService<CipherFileUploadService>("cipherFileUploadService"),
|
useFactory: getBgService<CipherFileUploadService>("cipherFileUploadService"),
|
||||||
@@ -434,11 +435,6 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: ProviderService,
|
|
||||||
useFactory: getBgService<ProviderService>("providerService"),
|
|
||||||
deps: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: SECURE_STORAGE,
|
provide: SECURE_STORAGE,
|
||||||
useFactory: getBgService<AbstractStorageService>("secureStorageService"),
|
useFactory: getBgService<AbstractStorageService>("secureStorageService"),
|
||||||
@@ -513,13 +509,15 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
) => {
|
) => {
|
||||||
return new ThemingService(
|
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
|
||||||
stateService,
|
// In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
|
||||||
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
|
let windowContext = window;
|
||||||
// In Safari we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
|
const backgroundWindow = BrowserApi.getBackgroundPage();
|
||||||
platformUtilsService.isSafari() ? getBgService<Window>("backgroundWindow")() : window,
|
if (platformUtilsService.isSafari() && backgroundWindow) {
|
||||||
document,
|
windowContext = backgroundWindow;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return new ThemingService(stateService, windowContext, document);
|
||||||
},
|
},
|
||||||
deps: [StateServiceAbstraction, PlatformUtilsService],
|
deps: [StateServiceAbstraction, PlatformUtilsService],
|
||||||
},
|
},
|
||||||
@@ -551,6 +549,11 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
useClass: AutofillSettingsService,
|
useClass: AutofillSettingsService,
|
||||||
deps: [StateProvider, PolicyService],
|
deps: [StateProvider, PolicyService],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: UserNotificationSettingsServiceAbstraction,
|
||||||
|
useClass: UserNotificationSettingsService,
|
||||||
|
deps: [StateProvider],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServicesModule {}
|
export class ServicesModule {}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
|
|||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
@@ -47,6 +48,7 @@ export class OptionsComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
@@ -95,10 +97,13 @@ export class OptionsComponent implements OnInit {
|
|||||||
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.enableAddLoginNotification = !(await this.stateService.getDisableAddLoginNotification());
|
this.enableAddLoginNotification = await firstValueFrom(
|
||||||
|
this.userNotificationSettingsService.enableAddedLoginPrompt$,
|
||||||
|
);
|
||||||
|
|
||||||
this.enableChangedPasswordNotification =
|
this.enableChangedPasswordNotification = await firstValueFrom(
|
||||||
!(await this.stateService.getDisableChangedPasswordNotification());
|
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
||||||
|
);
|
||||||
|
|
||||||
this.enableContextMenuItem = !(await this.stateService.getDisableContextMenuItem());
|
this.enableContextMenuItem = !(await this.stateService.getDisableContextMenuItem());
|
||||||
|
|
||||||
@@ -122,12 +127,14 @@ export class OptionsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAddLoginNotification() {
|
async updateAddLoginNotification() {
|
||||||
await this.stateService.setDisableAddLoginNotification(!this.enableAddLoginNotification);
|
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
|
||||||
|
this.enableAddLoginNotification,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateChangedPasswordNotification() {
|
async updateChangedPasswordNotification() {
|
||||||
await this.stateService.setDisableChangedPasswordNotification(
|
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
|
||||||
!this.enableChangedPasswordNotification,
|
this.enableChangedPasswordNotification,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums";
|
import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums";
|
||||||
|
|
||||||
|
type SuppressDownloadScriptInjectionConfig = {
|
||||||
|
file: string;
|
||||||
|
scriptingApiDetails?: { world: chrome.scripting.ExecutionWorld };
|
||||||
|
};
|
||||||
|
|
||||||
type FilelessImportPortMessage = {
|
type FilelessImportPortMessage = {
|
||||||
command?: string;
|
command?: string;
|
||||||
importType?: FilelessImportTypeKeys;
|
importType?: FilelessImportTypeKeys;
|
||||||
@@ -27,6 +32,7 @@ interface FilelessImporterBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
SuppressDownloadScriptInjectionConfig,
|
||||||
FilelessImportPortMessage,
|
FilelessImportPortMessage,
|
||||||
ImportNotificationMessageHandlers,
|
ImportNotificationMessageHandlers,
|
||||||
LpImporterMessageHandlers,
|
LpImporterMessageHandlers,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@@ -14,10 +15,20 @@ import {
|
|||||||
sendPortMessage,
|
sendPortMessage,
|
||||||
triggerRuntimeOnConnectEvent,
|
triggerRuntimeOnConnectEvent,
|
||||||
} from "../../autofill/spec/testing-utils";
|
} from "../../autofill/spec/testing-utils";
|
||||||
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums";
|
import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums";
|
||||||
|
|
||||||
import FilelessImporterBackground from "./fileless-importer.background";
|
import FilelessImporterBackground from "./fileless-importer.background";
|
||||||
|
|
||||||
|
jest.mock("rxjs", () => {
|
||||||
|
const rxjs = jest.requireActual("rxjs");
|
||||||
|
const { firstValueFrom } = rxjs;
|
||||||
|
return {
|
||||||
|
...rxjs,
|
||||||
|
firstValueFrom: jest.fn(firstValueFrom),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("FilelessImporterBackground ", () => {
|
describe("FilelessImporterBackground ", () => {
|
||||||
let filelessImporterBackground: FilelessImporterBackground;
|
let filelessImporterBackground: FilelessImporterBackground;
|
||||||
const configService = mock<ConfigService>();
|
const configService = mock<ConfigService>();
|
||||||
@@ -51,14 +62,17 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
|
|
||||||
describe("handle ports onConnect", () => {
|
describe("handle ports onConnect", () => {
|
||||||
let lpImporterPort: chrome.runtime.Port;
|
let lpImporterPort: chrome.runtime.Port;
|
||||||
|
let manifestVersionSpy: jest.SpyInstance;
|
||||||
|
let executeScriptInTabSpy: jest.SpyInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter);
|
lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter);
|
||||||
|
manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||||
|
executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null);
|
||||||
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
||||||
jest
|
jest.spyOn(filelessImporterBackground as any, "removeIndividualVault");
|
||||||
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
|
(firstValueFrom as jest.Mock).mockResolvedValue(false);
|
||||||
.mockResolvedValue(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => {
|
it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => {
|
||||||
@@ -83,9 +97,7 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => {
|
it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => {
|
||||||
jest
|
(firstValueFrom as jest.Mock).mockResolvedValue(true);
|
||||||
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
|
|
||||||
.mockResolvedValue(true);
|
|
||||||
|
|
||||||
triggerRuntimeOnConnectEvent(lpImporterPort);
|
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -117,6 +129,35 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
filelessImportEnabled: true,
|
filelessImportEnabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => {
|
||||||
|
manifestVersionSpy.mockReturnValue(3);
|
||||||
|
|
||||||
|
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(executeScriptInTabSpy).toHaveBeenCalledWith(
|
||||||
|
lpImporterPort.sender.tab.id,
|
||||||
|
{ file: "content/lp-suppress-import-download.js", runAt: "document_start" },
|
||||||
|
{ world: "MAIN" },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => {
|
||||||
|
manifestVersionSpy.mockReturnValue(2);
|
||||||
|
|
||||||
|
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(executeScriptInTabSpy).toHaveBeenCalledWith(
|
||||||
|
lpImporterPort.sender.tab.id,
|
||||||
|
{
|
||||||
|
file: "content/lp-suppress-import-download-script-append-mv2.js",
|
||||||
|
runAt: "document_start",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("port messages", () => {
|
describe("port messages", () => {
|
||||||
@@ -126,9 +167,7 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
||||||
jest
|
(firstValueFrom as jest.Mock).mockResolvedValue(false);
|
||||||
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
|
|
||||||
.mockResolvedValue(false);
|
|
||||||
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar));
|
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar));
|
||||||
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter));
|
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter));
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core";
|
|||||||
|
|
||||||
import NotificationBackground from "../../autofill/background/notification.background";
|
import NotificationBackground from "../../autofill/background/notification.background";
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts";
|
||||||
import {
|
import {
|
||||||
FilelessImportPort,
|
FilelessImportPort,
|
||||||
FilelessImportType,
|
FilelessImportType,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
LpImporterMessageHandlers,
|
LpImporterMessageHandlers,
|
||||||
FilelessImporterBackground as FilelessImporterBackgroundInterface,
|
FilelessImporterBackground as FilelessImporterBackgroundInterface,
|
||||||
FilelessImportPortMessage,
|
FilelessImportPortMessage,
|
||||||
|
SuppressDownloadScriptInjectionConfig,
|
||||||
} from "./abstractions/fileless-importer.background";
|
} from "./abstractions/fileless-importer.background";
|
||||||
|
|
||||||
class FilelessImporterBackground implements FilelessImporterBackgroundInterface {
|
class FilelessImporterBackground implements FilelessImporterBackgroundInterface {
|
||||||
@@ -108,6 +110,23 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
|
|||||||
await this.notificationBackground.requestFilelessImport(tab, importType);
|
await this.notificationBackground.requestFilelessImport(tab, importType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the script used to suppress the download of the LP importer export file.
|
||||||
|
*
|
||||||
|
* @param sender - The sender of the message.
|
||||||
|
* @param injectionConfig - The configuration for the injection.
|
||||||
|
*/
|
||||||
|
private async injectScriptConfig(
|
||||||
|
sender: chrome.runtime.MessageSender,
|
||||||
|
injectionConfig: SuppressDownloadScriptInjectionConfig,
|
||||||
|
) {
|
||||||
|
await BrowserApi.executeScriptInTab(
|
||||||
|
sender.tab.id,
|
||||||
|
{ file: injectionConfig.file, runAt: "document_start" },
|
||||||
|
injectionConfig.scriptingApiDetails,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers the download of the CSV file from the LP importer. This is triggered
|
* Triggers the download of the CSV file from the LP importer. This is triggered
|
||||||
* when the user opts to not save the export to Bitwarden within the notification bar.
|
* when the user opts to not save the export to Bitwarden within the notification bar.
|
||||||
@@ -200,6 +219,12 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
|
|||||||
switch (port.name) {
|
switch (port.name) {
|
||||||
case FilelessImportPort.LpImporter:
|
case FilelessImportPort.LpImporter:
|
||||||
this.lpImporterPort = port;
|
this.lpImporterPort = port;
|
||||||
|
await this.injectScriptConfig(
|
||||||
|
port.sender,
|
||||||
|
BrowserApi.manifestVersion === 3
|
||||||
|
? FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3
|
||||||
|
: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case FilelessImportPort.NotificationBar:
|
case FilelessImportPort.NotificationBar:
|
||||||
this.importNotificationsPort = port;
|
this.importNotificationsPort = port;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { SuppressDownloadScriptInjectionConfig } from "../background/abstractions/fileless-importer.background";
|
||||||
|
|
||||||
|
type FilelessImporterInjectedScriptsConfigurations = {
|
||||||
|
LpSuppressImportDownload: {
|
||||||
|
mv2: SuppressDownloadScriptInjectionConfig;
|
||||||
|
mv3: SuppressDownloadScriptInjectionConfig;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = {
|
||||||
|
LpSuppressImportDownload: {
|
||||||
|
mv2: {
|
||||||
|
file: "content/lp-suppress-import-download-script-append-mv2.js",
|
||||||
|
},
|
||||||
|
mv3: {
|
||||||
|
file: "content/lp-suppress-import-download.js",
|
||||||
|
scriptingApiDetails: { world: "MAIN" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export { FilelessImporterInjectedScriptsConfig };
|
||||||
@@ -43,20 +43,6 @@ describe("LpFilelessImporter", () => {
|
|||||||
expect(portSpy.disconnect).toHaveBeenCalled();
|
expect(portSpy.disconnect).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("injects a script element that suppresses the download of the LastPass export", () => {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
jest.spyOn(document, "createElement").mockReturnValue(script);
|
|
||||||
jest.spyOn(document.documentElement, "appendChild");
|
|
||||||
|
|
||||||
lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: true });
|
|
||||||
|
|
||||||
expect(document.createElement).toHaveBeenCalledWith("script");
|
|
||||||
expect(document.documentElement.appendChild).toHaveBeenCalled();
|
|
||||||
expect(script.textContent).toContain(
|
|
||||||
"const defaultAppendChild = Element.prototype.appendChild;",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => {
|
it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => {
|
||||||
Object.defineProperty(document, "readyState", {
|
Object.defineProperty(document, "readyState", {
|
||||||
value: "loading",
|
value: "loading",
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.suppressDownload();
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", this.loadImporter);
|
document.addEventListener("DOMContentLoaded", this.loadImporter);
|
||||||
return;
|
return;
|
||||||
@@ -52,46 +51,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface {
|
|||||||
this.postWindowMessage({ command: "triggerCsvDownload" });
|
this.postWindowMessage({ command: "triggerCsvDownload" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Suppresses the download of the CSV file by overriding the `download` attribute of the
|
|
||||||
* anchor element that is created by the LP importer. This is done by injecting a script
|
|
||||||
* into the page that overrides the `appendChild` method of the `Element` prototype.
|
|
||||||
*/
|
|
||||||
private suppressDownload() {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.textContent = `
|
|
||||||
let csvDownload = '';
|
|
||||||
let csvHref = '';
|
|
||||||
const defaultAppendChild = Element.prototype.appendChild;
|
|
||||||
Element.prototype.appendChild = function (newChild) {
|
|
||||||
if (newChild.nodeName.toLowerCase() === 'a' && newChild.download) {
|
|
||||||
csvDownload = newChild.download;
|
|
||||||
csvHref = newChild.href;
|
|
||||||
newChild.setAttribute('href', 'javascript:void(0)');
|
|
||||||
newChild.setAttribute('download', '');
|
|
||||||
Element.prototype.appendChild = defaultAppendChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultAppendChild.call(this, newChild);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('message', (event) => {
|
|
||||||
const command = event.data?.command;
|
|
||||||
if (event.source !== window || command !== 'triggerCsvDownload') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
anchor.setAttribute('href', csvHref);
|
|
||||||
anchor.setAttribute('download', csvDownload);
|
|
||||||
document.body.appendChild(anchor);
|
|
||||||
anchor.click();
|
|
||||||
document.body.removeChild(anchor);
|
|
||||||
});
|
|
||||||
`;
|
|
||||||
document.documentElement.appendChild(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the importing mechanism used to import the CSV file into Bitwarden.
|
* Initializes the importing mechanism used to import the CSV file into Bitwarden.
|
||||||
* This is done by observing the DOM for the addition of the LP importer element.
|
* This is done by observing the DOM for the addition of the LP importer element.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
describe("LP Suppress Import Download for Manifest v2", () => {
|
||||||
|
it("appends the `lp-suppress-import-download.js` script to the document element", () => {
|
||||||
|
let createdScriptElement: HTMLScriptElement;
|
||||||
|
jest.spyOn(window.document, "createElement");
|
||||||
|
jest.spyOn(window.document.documentElement, "appendChild").mockImplementation((node) => {
|
||||||
|
createdScriptElement = node as HTMLScriptElement;
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
require("./lp-suppress-import-download-script-append.mv2");
|
||||||
|
|
||||||
|
expect(window.document.createElement).toHaveBeenCalledWith("script");
|
||||||
|
expect(chrome.runtime.getURL).toHaveBeenCalledWith("content/lp-suppress-import-download.js");
|
||||||
|
expect(window.document.documentElement.appendChild).toHaveBeenCalledWith(
|
||||||
|
expect.any(HTMLScriptElement),
|
||||||
|
);
|
||||||
|
expect(createdScriptElement.src).toBe(
|
||||||
|
"chrome-extension://id/content/lp-suppress-import-download.js",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* This script handles injection of the LP suppress import download script into the document.
|
||||||
|
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
|
||||||
|
*/
|
||||||
|
(function (globalContext) {
|
||||||
|
const script = globalContext.document.createElement("script");
|
||||||
|
script.src = chrome.runtime.getURL("content/lp-suppress-import-download.js");
|
||||||
|
globalContext.document.documentElement.appendChild(script);
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { flushPromises, postWindowMessage } from "../../autofill/spec/testing-utils";
|
||||||
|
|
||||||
|
describe("LP Suppress Import Download", () => {
|
||||||
|
const downloadAttribute = "file.csv";
|
||||||
|
const hrefAttribute = "https://example.com/file.csv";
|
||||||
|
const overridenHrefAttribute = "javascript:void(0)";
|
||||||
|
let anchor: HTMLAnchorElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(Element.prototype, "appendChild");
|
||||||
|
jest.spyOn(window, "addEventListener");
|
||||||
|
|
||||||
|
require("./lp-suppress-import-download");
|
||||||
|
|
||||||
|
anchor = document.createElement("a");
|
||||||
|
anchor.download = downloadAttribute;
|
||||||
|
anchor.href = hrefAttribute;
|
||||||
|
anchor.click = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables the automatic download anchor", () => {
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
|
||||||
|
expect(anchor.href).toBe(overridenHrefAttribute);
|
||||||
|
expect(anchor.download).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers the CSVDownload when receiving a `triggerCsvDownload` window message", async () => {
|
||||||
|
window.document.createElement = jest.fn(() => anchor);
|
||||||
|
jest.spyOn(window, "removeEventListener");
|
||||||
|
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
|
||||||
|
// Precondition - Ensure the anchor in the document has overridden href and download attributes
|
||||||
|
expect(anchor.href).toBe(overridenHrefAttribute);
|
||||||
|
expect(anchor.download).toBe("");
|
||||||
|
|
||||||
|
postWindowMessage({ command: "triggerCsvDownload" });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(anchor.click).toHaveBeenCalled();
|
||||||
|
expect(anchor.href).toEqual(hrefAttribute);
|
||||||
|
expect(anchor.download).toEqual(downloadAttribute);
|
||||||
|
expect(window.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips subsequent calls to trigger a CSVDownload", async () => {
|
||||||
|
window.document.createElement = jest.fn(() => anchor);
|
||||||
|
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
|
||||||
|
postWindowMessage({ command: "triggerCsvDownload" });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
postWindowMessage({ command: "triggerCsvDownload" });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(anchor.click).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips triggering the CSV download for window messages that do not have the correct command", () => {
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
|
||||||
|
postWindowMessage({ command: "notTriggerCsvDownload" });
|
||||||
|
|
||||||
|
expect(anchor.click).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips triggering the CSV download for window messages that do not have a data value", () => {
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
|
||||||
|
postWindowMessage(null);
|
||||||
|
|
||||||
|
expect(anchor.click).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Handles intercepting the injection of the CSV download link, and ensures the
|
||||||
|
* download of the script is suppressed until the user opts to download the file.
|
||||||
|
* The download is triggered by a window message sent from the LpFilelessImporter
|
||||||
|
* content script.
|
||||||
|
*/
|
||||||
|
(function (globalContext) {
|
||||||
|
let csvDownload = "";
|
||||||
|
let csvHref = "";
|
||||||
|
let isCsvDownloadTriggered = false;
|
||||||
|
const defaultAppendChild = Element.prototype.appendChild;
|
||||||
|
Element.prototype.appendChild = function (newChild: Node) {
|
||||||
|
if (isAnchorElement(newChild) && newChild.download) {
|
||||||
|
csvDownload = newChild.download;
|
||||||
|
csvHref = newChild.href;
|
||||||
|
newChild.setAttribute("href", "javascript:void(0)");
|
||||||
|
newChild.setAttribute("download", "");
|
||||||
|
Element.prototype.appendChild = defaultAppendChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultAppendChild.call(this, newChild);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isAnchorElement(node: Node): node is HTMLAnchorElement {
|
||||||
|
return node.nodeName.toLowerCase() === "a";
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWindowMessage = (event: MessageEvent) => {
|
||||||
|
const command = event.data?.command;
|
||||||
|
if (
|
||||||
|
event.source !== globalContext ||
|
||||||
|
command !== "triggerCsvDownload" ||
|
||||||
|
isCsvDownloadTriggered
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCsvDownloadTriggered = true;
|
||||||
|
globalContext.removeEventListener("message", handleWindowMessage);
|
||||||
|
|
||||||
|
const anchor = globalContext.document.createElement("a");
|
||||||
|
anchor.setAttribute("href", csvHref);
|
||||||
|
anchor.setAttribute("download", csvDownload);
|
||||||
|
globalContext.document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
globalContext.document.body.removeChild(anchor);
|
||||||
|
};
|
||||||
|
|
||||||
|
globalContext.addEventListener("message", handleWindowMessage);
|
||||||
|
})(window);
|
||||||
@@ -179,6 +179,7 @@ const mainConfig = {
|
|||||||
"overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts",
|
"overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts",
|
||||||
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
|
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
|
||||||
"content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts",
|
"content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts",
|
||||||
|
"content/lp-suppress-import-download": "./src/tools/content/lp-suppress-import-download.ts",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: ENV !== "development",
|
minimize: ENV !== "development",
|
||||||
@@ -276,6 +277,8 @@ if (manifestVersion == 2) {
|
|||||||
// Manifest V2 background pages can be run through the regular build pipeline.
|
// Manifest V2 background pages can be run through the regular build pipeline.
|
||||||
// Since it's a standard webpage.
|
// Since it's a standard webpage.
|
||||||
mainConfig.entry.background = "./src/platform/background.ts";
|
mainConfig.entry.background = "./src/platform/background.ts";
|
||||||
|
mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] =
|
||||||
|
"./src/tools/content/lp-suppress-import-download-script-append.mv2.ts";
|
||||||
|
|
||||||
configs.push(mainConfig);
|
configs.push(mainConfig);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -60,11 +60,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
|
|||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
|
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||||
import {
|
import {
|
||||||
ActiveUserStateProvider,
|
ActiveUserStateProvider,
|
||||||
DerivedStateProvider,
|
DerivedStateProvider,
|
||||||
GlobalStateProvider,
|
GlobalStateProvider,
|
||||||
SingleUserStateProvider,
|
SingleUserStateProvider,
|
||||||
|
StateEventRunnerService,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
|
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
|
||||||
@@ -73,6 +75,7 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
|
|||||||
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
||||||
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
||||||
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||||
|
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||||
/* eslint-enable import/no-restricted-paths */
|
/* eslint-enable import/no-restricted-paths */
|
||||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||||
@@ -208,6 +211,7 @@ export class Main {
|
|||||||
derivedStateProvider: DerivedStateProvider;
|
derivedStateProvider: DerivedStateProvider;
|
||||||
stateProvider: StateProvider;
|
stateProvider: StateProvider;
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction;
|
loginStrategyService: LoginStrategyServiceAbstraction;
|
||||||
|
stateEventRunnerService: StateEventRunnerService;
|
||||||
biometricStateService: BiometricStateService;
|
biometricStateService: BiometricStateService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -249,14 +253,26 @@ export class Main {
|
|||||||
this.memoryStorageService = new MemoryStorageService();
|
this.memoryStorageService = new MemoryStorageService();
|
||||||
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
|
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
|
||||||
|
|
||||||
this.globalStateProvider = new DefaultGlobalStateProvider(
|
const storageServiceProvider = new StorageServiceProvider(
|
||||||
this.memoryStorageForStateProviders,
|
|
||||||
this.storageService,
|
this.storageService,
|
||||||
|
this.memoryStorageForStateProviders,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
|
||||||
|
|
||||||
|
const stateEventRegistrarService = new StateEventRegistrarService(
|
||||||
|
this.globalStateProvider,
|
||||||
|
storageServiceProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.stateEventRunnerService = new StateEventRunnerService(
|
||||||
|
this.globalStateProvider,
|
||||||
|
storageServiceProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
||||||
this.memoryStorageForStateProviders,
|
storageServiceProvider,
|
||||||
this.storageService,
|
stateEventRegistrarService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.messagingService = new NoopMessagingService();
|
this.messagingService = new NoopMessagingService();
|
||||||
@@ -269,8 +285,8 @@ export class Main {
|
|||||||
|
|
||||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.memoryStorageForStateProviders,
|
storageServiceProvider,
|
||||||
this.storageService,
|
stateEventRegistrarService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.derivedStateProvider = new DefaultDerivedStateProvider(
|
this.derivedStateProvider = new DefaultDerivedStateProvider(
|
||||||
@@ -372,7 +388,7 @@ export class Main {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.providerService = new ProviderService(this.stateService);
|
this.providerService = new ProviderService(this.stateProvider);
|
||||||
|
|
||||||
this.organizationService = new OrganizationService(this.stateService, this.stateProvider);
|
this.organizationService = new OrganizationService(this.stateService, this.stateProvider);
|
||||||
|
|
||||||
@@ -530,6 +546,7 @@ export class Main {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
this.authService,
|
this.authService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
|
this.stateEventRunnerService,
|
||||||
lockedCallback,
|
lockedCallback,
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -638,7 +655,11 @@ export class Main {
|
|||||||
this.collectionService.clear(userId as UserId),
|
this.collectionService.clear(userId as UserId),
|
||||||
this.policyService.clear(userId),
|
this.policyService.clear(userId),
|
||||||
this.passwordGenerationService.clear(),
|
this.passwordGenerationService.clear(),
|
||||||
|
this.providerService.save(null, userId as UserId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
||||||
|
|
||||||
await this.stateService.clean();
|
await this.stateService.clean();
|
||||||
process.env.BW_SESSION = null;
|
process.env.BW_SESSION = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { SettingsService } from "@bitwarden/common/abstractions/settings.service
|
|||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
@@ -39,6 +40,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
|
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -149,6 +151,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
|
private stateEventRunnerService: StateEventRunnerService,
|
||||||
|
private providerService: ProviderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -219,13 +223,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
const currentUser = await this.stateService.getUserId();
|
const currentUser = await this.stateService.getUserId();
|
||||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||||
await this.vaultTimeoutService.lock(currentUser);
|
await this.vaultTimeoutService.lock(currentUser);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
for (const account of Object.keys(accounts)) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
if (account === currentUser) {
|
||||||
Promise.all(
|
continue;
|
||||||
Object.keys(accounts)
|
}
|
||||||
.filter((u) => u !== currentUser)
|
|
||||||
.map((u) => this.vaultTimeoutService.lock(u)),
|
await this.vaultTimeoutService.lock(account);
|
||||||
);
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "locked":
|
case "locked":
|
||||||
@@ -582,6 +586,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
await this.policyService.clear(userBeingLoggedOut);
|
await this.policyService.clear(userBeingLoggedOut);
|
||||||
await this.keyConnectorService.clear();
|
await this.keyConnectorService.clear();
|
||||||
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
|
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
|
||||||
|
await this.providerService.save(null, userBeingLoggedOut as UserId);
|
||||||
|
|
||||||
|
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId);
|
||||||
|
|
||||||
preLogoutActiveUserId = this.activeUserId;
|
preLogoutActiveUserId = this.activeUserId;
|
||||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
|
|||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
|
||||||
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
|
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
|
||||||
|
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
||||||
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||||
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
||||||
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
||||||
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||||
|
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||||
/* eslint-enable import/no-restricted-paths */
|
/* eslint-enable import/no-restricted-paths */
|
||||||
|
|
||||||
@@ -104,10 +106,11 @@ export class Main {
|
|||||||
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
|
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
|
||||||
this.memoryStorageService = new MemoryStorageService();
|
this.memoryStorageService = new MemoryStorageService();
|
||||||
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
|
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
|
||||||
const globalStateProvider = new DefaultGlobalStateProvider(
|
const storageServiceProvider = new StorageServiceProvider(
|
||||||
this.memoryStorageForStateProviders,
|
|
||||||
this.storageService,
|
this.storageService,
|
||||||
|
this.memoryStorageForStateProviders,
|
||||||
);
|
);
|
||||||
|
const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
|
||||||
|
|
||||||
const accountService = new AccountServiceImplementation(
|
const accountService = new AccountServiceImplementation(
|
||||||
new NoopMessagingService(),
|
new NoopMessagingService(),
|
||||||
@@ -115,13 +118,18 @@ export class Main {
|
|||||||
globalStateProvider,
|
globalStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const stateEventRegistrarService = new StateEventRegistrarService(
|
||||||
|
globalStateProvider,
|
||||||
|
storageServiceProvider,
|
||||||
|
);
|
||||||
|
|
||||||
const stateProvider = new DefaultStateProvider(
|
const stateProvider = new DefaultStateProvider(
|
||||||
new DefaultActiveUserStateProvider(
|
new DefaultActiveUserStateProvider(
|
||||||
accountService,
|
accountService,
|
||||||
this.memoryStorageForStateProviders,
|
storageServiceProvider,
|
||||||
this.storageService,
|
stateEventRegistrarService,
|
||||||
),
|
),
|
||||||
new DefaultSingleUserStateProvider(this.memoryStorageForStateProviders, this.storageService),
|
new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
|
||||||
globalStateProvider,
|
globalStateProvider,
|
||||||
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
|
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/web-vault",
|
"name": "@bitwarden/web-vault",
|
||||||
"version": "2024.2.3",
|
"version": "2024.2.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:oss": "webpack",
|
"build:oss": "webpack",
|
||||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
canAccessBillingTab,
|
canAccessBillingTab,
|
||||||
canAccessGroupsTab,
|
canAccessGroupsTab,
|
||||||
canAccessMembersTab,
|
canAccessMembersTab,
|
||||||
|
canAccessOrgAdmin,
|
||||||
canAccessReportingTab,
|
canAccessReportingTab,
|
||||||
canAccessSettingsTab,
|
canAccessSettingsTab,
|
||||||
canAccessVaultTab,
|
canAccessVaultTab,
|
||||||
@@ -43,7 +44,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
|||||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||||
protected readonly logo = AdminConsoleLogo;
|
protected readonly logo = AdminConsoleLogo;
|
||||||
|
|
||||||
protected orgFilter = (org: Organization) => org.isAdmin;
|
protected orgFilter = (org: Organization) => canAccessOrgAdmin(org);
|
||||||
|
|
||||||
organization$: Observable<Organization>;
|
organization$: Observable<Organization>;
|
||||||
showPaymentAndHistory$: Observable<boolean>;
|
showPaymentAndHistory$: Observable<boolean>;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
|
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -89,6 +90,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
|
private stateEventRunnerService: StateEventRunnerService,
|
||||||
private paymentMethodWarningService: PaymentMethodWarningService,
|
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
) {}
|
) {}
|
||||||
@@ -284,6 +286,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.paymentMethodWarningService.clear(),
|
this.paymentMethodWarningService.clear(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
||||||
|
|
||||||
this.searchService.clearIndex();
|
this.searchService.clearIndex();
|
||||||
this.authService.logOut(async () => {
|
this.authService.logOut(async () => {
|
||||||
if (expired) {
|
if (expired) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
|
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
|
||||||
import { LoginService } from "@bitwarden/common/auth/services/login.service";
|
import { LoginService } from "@bitwarden/common/auth/services/login.service";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
@@ -28,21 +27,16 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
|||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import {
|
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
|
||||||
ActiveUserStateProvider,
|
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||||
GlobalStateProvider,
|
|
||||||
SingleUserStateProvider,
|
|
||||||
} from "@bitwarden/common/platform/state";
|
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
|
||||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||||
|
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
|
||||||
|
|
||||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||||
import { HtmlStorageService } from "../core/html-storage.service";
|
import { HtmlStorageService } from "../core/html-storage.service";
|
||||||
import { I18nService } from "../core/i18n.service";
|
import { I18nService } from "../core/i18n.service";
|
||||||
import { WebActiveUserStateProvider } from "../platform/web-active-user-state.provider";
|
|
||||||
import { WebGlobalStateProvider } from "../platform/web-global-state.provider";
|
|
||||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||||
import { WebSingleUserStateProvider } from "../platform/web-single-user-state.provider";
|
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
||||||
import { WindowStorageService } from "../platform/window-storage.service";
|
import { WindowStorageService } from "../platform/window-storage.service";
|
||||||
import { CollectionAdminService } from "../vault/core/collection-admin.service";
|
import { CollectionAdminService } from "../vault/core/collection-admin.service";
|
||||||
|
|
||||||
@@ -124,24 +118,9 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
|||||||
useFactory: () => new WindowStorageService(window.localStorage),
|
useFactory: () => new WindowStorageService(window.localStorage),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: SingleUserStateProvider,
|
provide: StorageServiceProvider,
|
||||||
useClass: WebSingleUserStateProvider,
|
useClass: WebStorageServiceProvider,
|
||||||
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
|
deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ActiveUserStateProvider,
|
|
||||||
useClass: WebActiveUserStateProvider,
|
|
||||||
deps: [
|
|
||||||
AccountService,
|
|
||||||
OBSERVABLE_MEMORY_STORAGE,
|
|
||||||
OBSERVABLE_DISK_STORAGE,
|
|
||||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: GlobalStateProvider,
|
|
||||||
useClass: WebGlobalStateProvider,
|
|
||||||
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: MigrationRunner,
|
provide: MigrationRunner,
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ export class ProductSwitcherContentComponent {
|
|||||||
|
|
||||||
// If the active route org doesn't have access to AC, find the first org that does.
|
// If the active route org doesn't have access to AC, find the first org that does.
|
||||||
const acOrg =
|
const acOrg =
|
||||||
routeOrg != null && canAccessOrgAdmin(routeOrg) && routeOrg.enabled
|
routeOrg != null && canAccessOrgAdmin(routeOrg)
|
||||||
? routeOrg
|
? routeOrg
|
||||||
: orgs.find((o) => canAccessOrgAdmin(o) && o.enabled);
|
: orgs.find((o) => canAccessOrgAdmin(o));
|
||||||
|
|
||||||
// TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092.
|
// TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092.
|
||||||
const providers = await this.providerService.getAll();
|
const providers = await this.providerService.getAll();
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import {
|
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
|
||||||
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code */
|
|
||||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
|
||||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
|
||||||
/* eslint-enable import/no-restricted-paths */
|
|
||||||
|
|
||||||
export class WebActiveUserStateProvider extends DefaultActiveUserStateProvider {
|
|
||||||
constructor(
|
|
||||||
accountService: AccountService,
|
|
||||||
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
|
||||||
sessionStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
) {
|
|
||||||
super(accountService, memoryStorage, sessionStorage);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
|
||||||
return (
|
|
||||||
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
|
|
||||||
keyDefinition.stateDefinition.defaultStorageLocation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override getLocation(
|
|
||||||
stateDefinition: StateDefinition,
|
|
||||||
): AbstractStorageService & ObservableStorageService {
|
|
||||||
const location =
|
|
||||||
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
|
|
||||||
switch (location) {
|
|
||||||
case "disk":
|
|
||||||
return this.diskStorage;
|
|
||||||
case "memory":
|
|
||||||
return this.memoryStorage;
|
|
||||||
case "disk-local":
|
|
||||||
return this.diskLocalStorage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import {
|
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { KeyDefinition } from "@bitwarden/common/platform/state";
|
|
||||||
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code*/
|
|
||||||
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
|
||||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
|
||||||
/* eslint-enable import/no-restricted-paths */
|
|
||||||
|
|
||||||
export class WebGlobalStateProvider extends DefaultGlobalStateProvider {
|
|
||||||
constructor(
|
|
||||||
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
|
||||||
sessionStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
) {
|
|
||||||
super(memoryStorage, sessionStorage);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
|
|
||||||
return (
|
|
||||||
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
|
|
||||||
keyDefinition.stateDefinition.defaultStorageLocation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override getLocation(
|
|
||||||
stateDefinition: StateDefinition,
|
|
||||||
): AbstractStorageService & ObservableStorageService {
|
|
||||||
const location =
|
|
||||||
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
|
|
||||||
switch (location) {
|
|
||||||
case "disk":
|
|
||||||
return this.diskStorage;
|
|
||||||
case "memory":
|
|
||||||
return this.memoryStorage;
|
|
||||||
case "disk-local":
|
|
||||||
return this.diskLocalStorage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import {
|
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
|
||||||
/* eslint-disable import/no-restricted-paths -- Needed to extend service & and in platform owned file */
|
|
||||||
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
|
||||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
|
||||||
/* eslint-enable import/no-restricted-paths */
|
|
||||||
|
|
||||||
export class WebSingleUserStateProvider extends DefaultSingleUserStateProvider {
|
|
||||||
constructor(
|
|
||||||
memoryStorageService: AbstractMemoryStorageService & ObservableStorageService,
|
|
||||||
sessionStorageService: AbstractStorageService & ObservableStorageService,
|
|
||||||
private readonly diskLocalStorageService: AbstractStorageService & ObservableStorageService,
|
|
||||||
) {
|
|
||||||
super(memoryStorageService, sessionStorageService);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
|
||||||
return (
|
|
||||||
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
|
|
||||||
keyDefinition.stateDefinition.defaultStorageLocation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override getLocation(
|
|
||||||
stateDefinition: StateDefinition,
|
|
||||||
): AbstractStorageService & ObservableStorageService {
|
|
||||||
const location =
|
|
||||||
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
|
|
||||||
|
|
||||||
switch (location) {
|
|
||||||
case "disk":
|
|
||||||
return this.diskStorage;
|
|
||||||
case "memory":
|
|
||||||
return this.memoryStorage;
|
|
||||||
case "disk-local":
|
|
||||||
return this.diskLocalStorageService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -135,6 +135,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
|
|||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
|
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
|
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
import {
|
import {
|
||||||
@@ -150,6 +151,8 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
|
|||||||
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
|
||||||
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
||||||
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
|
||||||
|
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||||
|
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
|
||||||
/* eslint-enable import/no-restricted-paths */
|
/* eslint-enable import/no-restricted-paths */
|
||||||
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||||
@@ -551,6 +554,7 @@ import { ModalService } from "./modal.service";
|
|||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
VaultTimeoutSettingsServiceAbstraction,
|
VaultTimeoutSettingsServiceAbstraction,
|
||||||
|
StateEventRunnerService,
|
||||||
LOCKED_CALLBACK,
|
LOCKED_CALLBACK,
|
||||||
LOGOUT_CALLBACK,
|
LOGOUT_CALLBACK,
|
||||||
],
|
],
|
||||||
@@ -741,7 +745,7 @@ import { ModalService } from "./modal.service";
|
|||||||
{
|
{
|
||||||
provide: ProviderServiceAbstraction,
|
provide: ProviderServiceAbstraction,
|
||||||
useClass: ProviderService,
|
useClass: ProviderService,
|
||||||
deps: [StateServiceAbstraction],
|
deps: [StateProvider],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: TwoFactorServiceAbstraction,
|
provide: TwoFactorServiceAbstraction,
|
||||||
@@ -890,20 +894,35 @@ import { ModalService } from "./modal.service";
|
|||||||
LogService,
|
LogService,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: StorageServiceProvider,
|
||||||
|
useClass: StorageServiceProvider,
|
||||||
|
deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: StateEventRegistrarService,
|
||||||
|
useClass: StateEventRegistrarService,
|
||||||
|
deps: [GlobalStateProvider, StorageServiceProvider],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: StateEventRunnerService,
|
||||||
|
useClass: StateEventRunnerService,
|
||||||
|
deps: [GlobalStateProvider, StorageServiceProvider],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: GlobalStateProvider,
|
provide: GlobalStateProvider,
|
||||||
useClass: DefaultGlobalStateProvider,
|
useClass: DefaultGlobalStateProvider,
|
||||||
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE],
|
deps: [StorageServiceProvider],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ActiveUserStateProvider,
|
provide: ActiveUserStateProvider,
|
||||||
useClass: DefaultActiveUserStateProvider,
|
useClass: DefaultActiveUserStateProvider,
|
||||||
deps: [AccountServiceAbstraction, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE],
|
deps: [AccountServiceAbstraction, StorageServiceProvider, StateEventRegistrarService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: SingleUserStateProvider,
|
provide: SingleUserStateProvider,
|
||||||
useClass: DefaultSingleUserStateProvider,
|
useClass: DefaultSingleUserStateProvider,
|
||||||
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE],
|
deps: [StorageServiceProvider, StateEventRegistrarService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: DerivedStateProvider,
|
provide: DerivedStateProvider,
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export class FakeGlobalStateProvider implements GlobalStateProvider {
|
|||||||
states: Map<string, GlobalState<unknown>> = new Map();
|
states: Map<string, GlobalState<unknown>> = new Map();
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
||||||
this.mock.get(keyDefinition);
|
this.mock.get(keyDefinition);
|
||||||
let result = this.states.get(keyDefinition.fullName);
|
const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
|
||||||
|
let result = this.states.get(cacheKey);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
let fake: FakeGlobalState<T>;
|
let fake: FakeGlobalState<T>;
|
||||||
@@ -44,10 +45,10 @@ export class FakeGlobalStateProvider implements GlobalStateProvider {
|
|||||||
}
|
}
|
||||||
fake.keyDefinition = keyDefinition;
|
fake.keyDefinition = keyDefinition;
|
||||||
result = fake;
|
result = fake;
|
||||||
this.states.set(keyDefinition.fullName, result);
|
this.states.set(cacheKey, result);
|
||||||
|
|
||||||
result = new FakeGlobalState<T>();
|
result = new FakeGlobalState<T>();
|
||||||
this.states.set(keyDefinition.fullName, result);
|
this.states.set(cacheKey, result);
|
||||||
}
|
}
|
||||||
return result as GlobalState<T>;
|
return result as GlobalState<T>;
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,8 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
if (keyDefinition instanceof KeyDefinition) {
|
if (keyDefinition instanceof KeyDefinition) {
|
||||||
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
}
|
}
|
||||||
let result = this.states.get(`${keyDefinition.fullName}_${userId}`);
|
const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}_${userId}`;
|
||||||
|
let result = this.states.get(cacheKey);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
let fake: FakeSingleUserState<T>;
|
let fake: FakeSingleUserState<T>;
|
||||||
@@ -88,7 +90,7 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
}
|
}
|
||||||
fake.keyDefinition = keyDefinition;
|
fake.keyDefinition = keyDefinition;
|
||||||
result = fake;
|
result = fake;
|
||||||
this.states.set(`${keyDefinition.fullName}_${userId}`, result);
|
this.states.set(cacheKey, result);
|
||||||
}
|
}
|
||||||
return result as SingleUserState<T>;
|
return result as SingleUserState<T>;
|
||||||
}
|
}
|
||||||
@@ -119,7 +121,8 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
if (keyDefinition instanceof KeyDefinition) {
|
if (keyDefinition instanceof KeyDefinition) {
|
||||||
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
}
|
}
|
||||||
let result = this.states.get(keyDefinition.fullName);
|
const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
|
||||||
|
let result = this.states.get(cacheKey);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
// Look for established mock
|
// Look for established mock
|
||||||
@@ -129,7 +132,7 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
result = new FakeActiveUserState<T>(this.accountService);
|
result = new FakeActiveUserState<T>(this.accountService);
|
||||||
}
|
}
|
||||||
result.keyDefinition = keyDefinition;
|
result.keyDefinition = keyDefinition;
|
||||||
this.states.set(keyDefinition.fullName, result);
|
this.states.set(cacheKey, result);
|
||||||
}
|
}
|
||||||
return result as ActiveUserState<T>;
|
return result as ActiveUserState<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export class FakeGlobalState<T> implements GlobalState<T> {
|
|||||||
this.nextMock(newState);
|
this.nextMock(newState);
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tracks update values resolved by `FakeState.update` */
|
/** Tracks update values resolved by `FakeState.update` */
|
||||||
nextMock = jest.fn<void, [T]>();
|
nextMock = jest.fn<void, [T]>();
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export * from "./utils";
|
|||||||
export * from "./intercept-console";
|
export * from "./intercept-console";
|
||||||
export * from "./matchers";
|
export * from "./matchers";
|
||||||
export * from "./fake-state-provider";
|
export * from "./fake-state-provider";
|
||||||
|
export * from "./fake-state";
|
||||||
export * from "./fake-account-service";
|
export * from "./fake-account-service";
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { ProviderData } from "../models/data/provider.data";
|
import { ProviderData } from "../models/data/provider.data";
|
||||||
import { Provider } from "../models/domain/provider";
|
import { Provider } from "../models/domain/provider";
|
||||||
|
|
||||||
export abstract class ProviderService {
|
export abstract class ProviderService {
|
||||||
get: (id: string) => Promise<Provider>;
|
get: (id: string) => Promise<Provider>;
|
||||||
getAll: () => Promise<Provider[]>;
|
getAll: () => Promise<Provider[]>;
|
||||||
save: (providers: { [id: string]: ProviderData }) => Promise<any>;
|
save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,56 @@
|
|||||||
|
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||||
|
import { FakeActiveUserState } from "../../../spec/fake-state";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { ProviderUserStatusType, ProviderUserType } from "../enums";
|
import { ProviderUserStatusType, ProviderUserType } from "../enums";
|
||||||
import { ProviderData } from "../models/data/provider.data";
|
import { ProviderData } from "../models/data/provider.data";
|
||||||
|
import { Provider } from "../models/domain/provider";
|
||||||
|
|
||||||
import { PROVIDERS } from "./provider.service";
|
import { PROVIDERS, ProviderService } from "./provider.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is easier to read arrays than records in code, but we store a record
|
||||||
|
* in state. This helper methods lets us build provider arrays in tests
|
||||||
|
* and easily map them to records before storing them in state.
|
||||||
|
*/
|
||||||
|
function arrayToRecord(input: ProviderData[]): Record<string, ProviderData> {
|
||||||
|
if (input == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Object.fromEntries(input?.map((i) => [i.id, i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a simple mock `ProviderData[]` array that can be used in tests
|
||||||
|
* to populate state.
|
||||||
|
* @param count The number of organizations to populate the list with. The
|
||||||
|
* function returns undefined if this is less than 1. The default value is 1.
|
||||||
|
* @param suffix A string to append to data fields on each provider.
|
||||||
|
* This defaults to the index of the organization in the list.
|
||||||
|
* @returns a `ProviderData[]` array that can be used to populate
|
||||||
|
* stateProvider.
|
||||||
|
*/
|
||||||
|
function buildMockProviders(count = 1, suffix?: string): ProviderData[] {
|
||||||
|
if (count < 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMockProvider(id: string, name: string): ProviderData {
|
||||||
|
const data = new ProviderData({} as any);
|
||||||
|
data.id = id;
|
||||||
|
data.name = name;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockProviders = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const s = suffix ? suffix + i.toString() : i.toString();
|
||||||
|
mockProviders.push(buildMockProvider("provider" + s, "provider" + s));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockProviders;
|
||||||
|
}
|
||||||
|
|
||||||
describe("PROVIDERS key definition", () => {
|
describe("PROVIDERS key definition", () => {
|
||||||
const sut = PROVIDERS;
|
const sut = PROVIDERS;
|
||||||
@@ -21,3 +70,75 @@ describe("PROVIDERS key definition", () => {
|
|||||||
expect(result).toEqual(expectedResult);
|
expect(result).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ProviderService", () => {
|
||||||
|
let providerService: ProviderService;
|
||||||
|
|
||||||
|
const fakeUserId = Utils.newGuid() as UserId;
|
||||||
|
let fakeAccountService: FakeAccountService;
|
||||||
|
let fakeStateProvider: FakeStateProvider;
|
||||||
|
let fakeActiveUserState: FakeActiveUserState<Record<string, ProviderData>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
fakeAccountService = mockAccountServiceWith(fakeUserId);
|
||||||
|
fakeStateProvider = new FakeStateProvider(fakeAccountService);
|
||||||
|
fakeActiveUserState = fakeStateProvider.activeUser.getFake(PROVIDERS);
|
||||||
|
providerService = new ProviderService(fakeStateProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAll()", () => {
|
||||||
|
it("Returns an array of all providers stored in state", async () => {
|
||||||
|
const mockData: ProviderData[] = buildMockProviders(5);
|
||||||
|
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
||||||
|
const providers = await providerService.getAll();
|
||||||
|
expect(providers).toHaveLength(5);
|
||||||
|
expect(providers).toEqual(mockData.map((x) => new Provider(x)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns an empty array if no providers are found in state", async () => {
|
||||||
|
const mockData: ProviderData[] = undefined;
|
||||||
|
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
||||||
|
const result = await providerService.getAll();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("get()", () => {
|
||||||
|
it("Returns a single provider from state that matches the specified id", async () => {
|
||||||
|
const mockData = buildMockProviders(5);
|
||||||
|
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
||||||
|
const result = await providerService.get(mockData[3].id);
|
||||||
|
expect(result).toEqual(new Provider(mockData[3]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns undefined if the specified provider id is not found", async () => {
|
||||||
|
const result = await providerService.get("this-provider-does-not-exist");
|
||||||
|
expect(result).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("save()", () => {
|
||||||
|
it("replaces the entire provider list in state for the active user", async () => {
|
||||||
|
const originalData = buildMockProviders(10);
|
||||||
|
fakeActiveUserState.nextState(arrayToRecord(originalData));
|
||||||
|
|
||||||
|
const newData = buildMockProviders(10, "newData");
|
||||||
|
await providerService.save(arrayToRecord(newData));
|
||||||
|
|
||||||
|
const result = await providerService.getAll();
|
||||||
|
|
||||||
|
expect(result).toEqual(newData);
|
||||||
|
expect(result).not.toEqual(originalData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is more or less a test for logouts
|
||||||
|
it("can replace state with null", async () => {
|
||||||
|
const originalData = buildMockProviders(2);
|
||||||
|
fakeActiveUserState.nextState(arrayToRecord(originalData));
|
||||||
|
await providerService.save(null);
|
||||||
|
const result = await providerService.getAll();
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(result).not.toEqual(originalData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { Observable, map, firstValueFrom } from "rxjs";
|
||||||
import { KeyDefinition, PROVIDERS_DISK } from "../../platform/state";
|
|
||||||
|
import { KeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
|
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
|
||||||
import { ProviderData } from "../models/data/provider.data";
|
import { ProviderData } from "../models/data/provider.data";
|
||||||
import { Provider } from "../models/domain/provider";
|
import { Provider } from "../models/domain/provider";
|
||||||
@@ -8,32 +10,34 @@ export const PROVIDERS = KeyDefinition.record<ProviderData>(PROVIDERS_DISK, "pro
|
|||||||
deserializer: (obj: ProviderData) => obj,
|
deserializer: (obj: ProviderData) => obj,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function mapToSingleProvider(providerId: string) {
|
||||||
|
return map<Provider[], Provider>((providers) => providers?.find((p) => p.id === providerId));
|
||||||
|
}
|
||||||
|
|
||||||
export class ProviderService implements ProviderServiceAbstraction {
|
export class ProviderService implements ProviderServiceAbstraction {
|
||||||
constructor(private stateService: StateService) {}
|
constructor(private stateProvider: StateProvider) {}
|
||||||
|
|
||||||
|
private providers$(userId?: UserId): Observable<Provider[] | undefined> {
|
||||||
|
return this.stateProvider
|
||||||
|
.getUserState$(PROVIDERS, userId)
|
||||||
|
.pipe(this.mapProviderRecordToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapProviderRecordToArray() {
|
||||||
|
return map<Record<string, ProviderData>, Provider[]>((providers) =>
|
||||||
|
Object.values(providers ?? {})?.map((o) => new Provider(o)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async get(id: string): Promise<Provider> {
|
async get(id: string): Promise<Provider> {
|
||||||
const providers = await this.stateService.getProviders();
|
return await firstValueFrom(this.providers$().pipe(mapToSingleProvider(id)));
|
||||||
// eslint-disable-next-line
|
|
||||||
if (providers == null || !providers.hasOwnProperty(id)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Provider(providers[id]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<Provider[]> {
|
async getAll(): Promise<Provider[]> {
|
||||||
const providers = await this.stateService.getProviders();
|
return await firstValueFrom(this.providers$());
|
||||||
const response: Provider[] = [];
|
|
||||||
for (const id in providers) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (providers.hasOwnProperty(id)) {
|
|
||||||
response.push(new Provider(providers[id]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(providers: { [id: string]: ProviderData }) {
|
async save(providers: { [id: string]: ProviderData }, userId?: UserId) {
|
||||||
await this.stateService.setProviders(providers);
|
await this.stateProvider.setUserState(PROVIDERS, providers, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
USER_NOTIFICATION_SETTINGS_DISK,
|
||||||
|
GlobalState,
|
||||||
|
KeyDefinition,
|
||||||
|
StateProvider,
|
||||||
|
} from "../../platform/state";
|
||||||
|
|
||||||
|
const ENABLE_ADDED_LOGIN_PROMPT = new KeyDefinition(
|
||||||
|
USER_NOTIFICATION_SETTINGS_DISK,
|
||||||
|
"enableAddedLoginPrompt",
|
||||||
|
{
|
||||||
|
deserializer: (value: boolean) => value ?? true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const ENABLE_CHANGED_PASSWORD_PROMPT = new KeyDefinition(
|
||||||
|
USER_NOTIFICATION_SETTINGS_DISK,
|
||||||
|
"enableChangedPasswordPrompt",
|
||||||
|
{
|
||||||
|
deserializer: (value: boolean) => value ?? true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export abstract class UserNotificationSettingsServiceAbstraction {
|
||||||
|
enableAddedLoginPrompt$: Observable<boolean>;
|
||||||
|
setEnableAddedLoginPrompt: (newValue: boolean) => Promise<void>;
|
||||||
|
enableChangedPasswordPrompt$: Observable<boolean>;
|
||||||
|
setEnableChangedPasswordPrompt: (newValue: boolean) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserNotificationSettingsService implements UserNotificationSettingsServiceAbstraction {
|
||||||
|
private enableAddedLoginPromptState: GlobalState<boolean>;
|
||||||
|
readonly enableAddedLoginPrompt$: Observable<boolean>;
|
||||||
|
|
||||||
|
private enableChangedPasswordPromptState: GlobalState<boolean>;
|
||||||
|
readonly enableChangedPasswordPrompt$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(private stateProvider: StateProvider) {
|
||||||
|
this.enableAddedLoginPromptState = this.stateProvider.getGlobal(ENABLE_ADDED_LOGIN_PROMPT);
|
||||||
|
this.enableAddedLoginPrompt$ = this.enableAddedLoginPromptState.state$.pipe(
|
||||||
|
map((x) => x ?? true),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.enableChangedPasswordPromptState = this.stateProvider.getGlobal(
|
||||||
|
ENABLE_CHANGED_PASSWORD_PROMPT,
|
||||||
|
);
|
||||||
|
this.enableChangedPasswordPrompt$ = this.enableChangedPasswordPromptState.state$.pipe(
|
||||||
|
map((x) => x ?? true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEnableAddedLoginPrompt(newValue: boolean): Promise<void> {
|
||||||
|
await this.enableAddedLoginPromptState.update(() => newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEnableChangedPasswordPrompt(newValue: boolean): Promise<void> {
|
||||||
|
await this.enableChangedPasswordPromptState.update(() => newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import { Observable } from "rxjs";
|
|||||||
|
|
||||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||||
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
||||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
|
||||||
import { Policy } from "../../admin-console/models/domain/policy";
|
import { Policy } from "../../admin-console/models/domain/policy";
|
||||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
||||||
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
||||||
@@ -200,13 +199,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
|
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
|
||||||
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
|
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
|
||||||
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
||||||
getDisableAddLoginNotification: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setDisableAddLoginNotification: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setDisableChangedPasswordNotification: (
|
|
||||||
value: boolean,
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<void>;
|
|
||||||
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
|
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
@@ -378,8 +370,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
* Sets the user's Pin, encrypted by the user key
|
* Sets the user's Pin, encrypted by the user key
|
||||||
*/
|
*/
|
||||||
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
|
|
||||||
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
|
|
||||||
getRefreshToken: (options?: StorageOptions) => Promise<string>;
|
getRefreshToken: (options?: StorageOptions) => Promise<string>;
|
||||||
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
|
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
|
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Jsonify } from "type-fest";
|
|||||||
|
|
||||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||||
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
||||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||||
@@ -96,7 +95,6 @@ export class AccountData {
|
|||||||
addEditCipherInfo?: AddEditCipherInfo;
|
addEditCipherInfo?: AddEditCipherInfo;
|
||||||
eventCollection?: EventData[];
|
eventCollection?: EventData[];
|
||||||
organizations?: { [id: string]: OrganizationData };
|
organizations?: { [id: string]: OrganizationData };
|
||||||
providers?: { [id: string]: ProviderData };
|
|
||||||
|
|
||||||
static fromJSON(obj: DeepJsonify<AccountData>): AccountData {
|
static fromJSON(obj: DeepJsonify<AccountData>): AccountData {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ export class GlobalState {
|
|||||||
enableBrowserIntegrationFingerprint?: boolean;
|
enableBrowserIntegrationFingerprint?: boolean;
|
||||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||||
neverDomains?: { [id: string]: unknown };
|
neverDomains?: { [id: string]: unknown };
|
||||||
disableAddLoginNotification?: boolean;
|
|
||||||
disableChangedPasswordNotification?: boolean;
|
|
||||||
disableContextMenuItem?: boolean;
|
disableContextMenuItem?: boolean;
|
||||||
deepLinkRedirectUrl?: string;
|
deepLinkRedirectUrl?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom, timeout } from "rxjs";
|
import { firstValueFrom, timeout } from "rxjs";
|
||||||
|
|
||||||
import { awaitAsync } from "../../../spec";
|
import { awaitAsync } from "../../../spec";
|
||||||
@@ -14,9 +15,11 @@ import { DefaultDerivedStateProvider } from "../state/implementations/default-de
|
|||||||
import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider";
|
import { DefaultGlobalStateProvider } from "../state/implementations/default-global-state.provider";
|
||||||
import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider";
|
import { DefaultSingleUserStateProvider } from "../state/implementations/default-single-user-state.provider";
|
||||||
import { DefaultStateProvider } from "../state/implementations/default-state.provider";
|
import { DefaultStateProvider } from "../state/implementations/default-state.provider";
|
||||||
/* eslint-disable import/no-restricted-paths */
|
import { StateEventRegistrarService } from "../state/state-event-registrar.service";
|
||||||
|
/* eslint-enable import/no-restricted-paths */
|
||||||
|
|
||||||
import { EnvironmentService } from "./environment.service";
|
import { EnvironmentService } from "./environment.service";
|
||||||
|
import { StorageServiceProvider } from "./storage-service.provider";
|
||||||
|
|
||||||
// There are a few main states EnvironmentService could be in when first used
|
// There are a few main states EnvironmentService could be in when first used
|
||||||
// 1. Not initialized, no active user. Hopefully not to likely but possible
|
// 1. Not initialized, no active user. Hopefully not to likely but possible
|
||||||
@@ -26,6 +29,8 @@ import { EnvironmentService } from "./environment.service";
|
|||||||
describe("EnvironmentService", () => {
|
describe("EnvironmentService", () => {
|
||||||
let diskStorageService: FakeStorageService;
|
let diskStorageService: FakeStorageService;
|
||||||
let memoryStorageService: FakeStorageService;
|
let memoryStorageService: FakeStorageService;
|
||||||
|
let storageServiceProvider: StorageServiceProvider;
|
||||||
|
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
||||||
let accountService: FakeAccountService;
|
let accountService: FakeAccountService;
|
||||||
let stateProvider: StateProvider;
|
let stateProvider: StateProvider;
|
||||||
|
|
||||||
@@ -37,16 +42,17 @@ describe("EnvironmentService", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
diskStorageService = new FakeStorageService();
|
diskStorageService = new FakeStorageService();
|
||||||
memoryStorageService = new FakeStorageService();
|
memoryStorageService = new FakeStorageService();
|
||||||
|
storageServiceProvider = new StorageServiceProvider(diskStorageService, memoryStorageService);
|
||||||
|
|
||||||
accountService = mockAccountServiceWith(undefined);
|
accountService = mockAccountServiceWith(undefined);
|
||||||
stateProvider = new DefaultStateProvider(
|
stateProvider = new DefaultStateProvider(
|
||||||
new DefaultActiveUserStateProvider(
|
new DefaultActiveUserStateProvider(
|
||||||
accountService,
|
accountService,
|
||||||
memoryStorageService as any,
|
storageServiceProvider,
|
||||||
diskStorageService,
|
stateEventRegistrarService,
|
||||||
),
|
),
|
||||||
new DefaultSingleUserStateProvider(memoryStorageService as any, diskStorageService),
|
new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
|
||||||
new DefaultGlobalStateProvider(memoryStorageService as any, diskStorageService),
|
new DefaultGlobalStateProvider(storageServiceProvider),
|
||||||
new DefaultDerivedStateProvider(memoryStorageService),
|
new DefaultDerivedStateProvider(memoryStorageService),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest";
|
|||||||
|
|
||||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||||
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
||||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
|
||||||
import { Policy } from "../../admin-console/models/domain/policy";
|
import { Policy } from "../../admin-console/models/domain/policy";
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
@@ -853,45 +852,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDisableAddLoginNotification(options?: StorageOptions): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
|
||||||
?.disableAddLoginNotification ?? false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDisableAddLoginNotification(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
globals.disableAddLoginNotification = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDisableChangedPasswordNotification(options?: StorageOptions): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
|
||||||
?.disableChangedPasswordNotification ?? false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDisableChangedPasswordNotification(
|
|
||||||
value: boolean,
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
globals.disableChangedPasswordNotification = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDisableContextMenuItem(options?: StorageOptions): Promise<boolean> {
|
async getDisableContextMenuItem(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||||
@@ -1860,27 +1820,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototypeForObjectValues(ProviderData)
|
|
||||||
async getProviders(options?: StorageOptions): Promise<{ [id: string]: ProviderData }> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.data?.providers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setProviders(
|
|
||||||
value: { [id: string]: ProviderData },
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.data.providers = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRefreshToken(options?: StorageOptions): Promise<string> {
|
async getRefreshToken(options?: StorageOptions): Promise<string> {
|
||||||
options = await this.getTimeoutBasedStorageOptions(options);
|
options = await this.getTimeoutBasedStorageOptions(options);
|
||||||
return (await this.getAccount(options))?.tokens?.refreshToken;
|
return (await this.getAccount(options))?.tokens?.refreshToken;
|
||||||
|
|||||||
@@ -3,17 +3,14 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
|
import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import {
|
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||||
AbstractMemoryStorageService,
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "../../abstractions/storage.service";
|
|
||||||
|
|
||||||
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
|
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
|
||||||
|
|
||||||
describe("DefaultActiveUserStateProvider", () => {
|
describe("DefaultActiveUserStateProvider", () => {
|
||||||
const memoryStorage = mock<AbstractMemoryStorageService & ObservableStorageService>();
|
const storageServiceProvider = mock<StorageServiceProvider>();
|
||||||
const diskStorage = mock<AbstractStorageService & ObservableStorageService>();
|
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
||||||
const userId = "userId" as UserId;
|
const userId = "userId" as UserId;
|
||||||
const accountInfo = {
|
const accountInfo = {
|
||||||
id: userId,
|
id: userId,
|
||||||
@@ -25,7 +22,11 @@ describe("DefaultActiveUserStateProvider", () => {
|
|||||||
let sut: DefaultActiveUserStateProvider;
|
let sut: DefaultActiveUserStateProvider;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sut = new DefaultActiveUserStateProvider(accountService, memoryStorage, diskStorage);
|
sut = new DefaultActiveUserStateProvider(
|
||||||
|
accountService,
|
||||||
|
storageServiceProvider,
|
||||||
|
stateEventRegistrarService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ import { Observable, map } from "rxjs";
|
|||||||
|
|
||||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import {
|
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "../../abstractions/storage.service";
|
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
|
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
|
||||||
import { ActiveUserState } from "../user-state";
|
import { ActiveUserState } from "../user-state";
|
||||||
import { ActiveUserStateProvider } from "../user-state.provider";
|
import { ActiveUserStateProvider } from "../user-state.provider";
|
||||||
@@ -21,9 +17,9 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
activeUserId$: Observable<UserId | undefined>;
|
activeUserId$: Observable<UserId | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
private readonly storageServiceProvider: StorageServiceProvider,
|
||||||
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
private readonly stateEventRegistrarService: StateEventRegistrarService,
|
||||||
) {
|
) {
|
||||||
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
|
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
|
||||||
}
|
}
|
||||||
@@ -32,7 +28,11 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
if (!isUserKeyDefinition(keyDefinition)) {
|
if (!isUserKeyDefinition(keyDefinition)) {
|
||||||
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
}
|
}
|
||||||
const cacheKey = this.buildCacheKey(keyDefinition);
|
const [location, storageService] = this.storageServiceProvider.get(
|
||||||
|
keyDefinition.stateDefinition.defaultStorageLocation,
|
||||||
|
keyDefinition.stateDefinition.storageLocationOverrides,
|
||||||
|
);
|
||||||
|
const cacheKey = this.buildCacheKey(location, keyDefinition);
|
||||||
const existingUserState = this.cache[cacheKey];
|
const existingUserState = this.cache[cacheKey];
|
||||||
if (existingUserState != null) {
|
if (existingUserState != null) {
|
||||||
// I have to cast out of the unknown generic but this should be safe if rules
|
// I have to cast out of the unknown generic but this should be safe if rules
|
||||||
@@ -40,36 +40,17 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
return existingUserState as ActiveUserState<T>;
|
return existingUserState as ActiveUserState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUserState = this.buildActiveUserState(keyDefinition);
|
const newUserState = new DefaultActiveUserState<T>(
|
||||||
|
keyDefinition,
|
||||||
|
this.accountService,
|
||||||
|
storageService,
|
||||||
|
this.stateEventRegistrarService,
|
||||||
|
);
|
||||||
this.cache[cacheKey] = newUserState;
|
this.cache[cacheKey] = newUserState;
|
||||||
return newUserState;
|
return newUserState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCacheKey(keyDefinition: UserKeyDefinition<unknown>) {
|
private buildCacheKey(location: string, keyDefinition: UserKeyDefinition<unknown>) {
|
||||||
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`;
|
return `${location}_${keyDefinition.fullName}`;
|
||||||
}
|
|
||||||
|
|
||||||
protected buildActiveUserState<T>(keyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
|
|
||||||
return new DefaultActiveUserState<T>(
|
|
||||||
keyDefinition,
|
|
||||||
this.accountService,
|
|
||||||
this.getLocation(keyDefinition.stateDefinition),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
|
||||||
return keyDefinition.stateDefinition.defaultStorageLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getLocation(stateDefinition: StateDefinition) {
|
|
||||||
// The default implementations don't support the client overrides
|
|
||||||
// it is up to the client to extend this class and add that support
|
|
||||||
const location = stateDefinition.defaultStorageLocation;
|
|
||||||
switch (location) {
|
|
||||||
case "disk":
|
|
||||||
return this.diskStorage;
|
|
||||||
case "memory":
|
|
||||||
return this.memoryStorage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { AccountInfo, AccountService } from "../../../auth/abstractions/account.
|
|||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
import { UserKeyDefinition } from "../user-key-definition";
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
|
|
||||||
import { DefaultActiveUserState } from "./default-active-user-state";
|
import { DefaultActiveUserState } from "./default-active-user-state";
|
||||||
@@ -42,6 +43,7 @@ const testKeyDefinition = new UserKeyDefinition<TestState>(testStateDefinition,
|
|||||||
describe("DefaultActiveUserState", () => {
|
describe("DefaultActiveUserState", () => {
|
||||||
const accountService = mock<AccountService>();
|
const accountService = mock<AccountService>();
|
||||||
let diskStorageService: FakeStorageService;
|
let diskStorageService: FakeStorageService;
|
||||||
|
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
||||||
let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>;
|
let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>;
|
||||||
let userState: DefaultActiveUserState<TestState>;
|
let userState: DefaultActiveUserState<TestState>;
|
||||||
|
|
||||||
@@ -50,7 +52,12 @@ describe("DefaultActiveUserState", () => {
|
|||||||
accountService.activeAccount$ = activeAccountSubject;
|
accountService.activeAccount$ = activeAccountSubject;
|
||||||
|
|
||||||
diskStorageService = new FakeStorageService();
|
diskStorageService = new FakeStorageService();
|
||||||
userState = new DefaultActiveUserState(testKeyDefinition, accountService, diskStorageService);
|
userState = new DefaultActiveUserState(
|
||||||
|
testKeyDefinition,
|
||||||
|
accountService,
|
||||||
|
diskStorageService,
|
||||||
|
stateEventRegistrarService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeUserId = (id: string) => {
|
const makeUserId = (id: string) => {
|
||||||
@@ -391,6 +398,48 @@ describe("DefaultActiveUserState", () => {
|
|||||||
"No active user at this time.",
|
"No active user at this time.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"should register user key definition when state transitions from null-ish (%s) to non-null",
|
||||||
|
async (startingValue: TestState | null) => {
|
||||||
|
diskStorageService.internalUpdateStore({
|
||||||
|
"user_00000000-0000-1000-a000-000000000001_fake_fake": startingValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
await userState.update(() => ({ array: ["one"], date: new Date() }));
|
||||||
|
|
||||||
|
expect(stateEventRegistrarService.registerEvents).toHaveBeenCalledWith(testKeyDefinition);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should not register user key definition when state has preexisting value", async () => {
|
||||||
|
diskStorageService.internalUpdateStore({
|
||||||
|
"user_00000000-0000-1000-a000-000000000001_fake_fake": {
|
||||||
|
date: new Date(2019, 1),
|
||||||
|
array: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await userState.update(() => ({ array: ["one"], date: new Date() }));
|
||||||
|
|
||||||
|
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"should not register user key definition when setting value to null-ish (%s) value",
|
||||||
|
async (updatedValue: TestState | null) => {
|
||||||
|
diskStorageService.internalUpdateStore({
|
||||||
|
"user_00000000-0000-1000-a000-000000000001_fake_fake": {
|
||||||
|
date: new Date(2019, 1),
|
||||||
|
array: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await userState.update(() => updatedValue);
|
||||||
|
|
||||||
|
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("update races", () => {
|
describe("update races", () => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "../../abstractions/storage.service";
|
} from "../../abstractions/storage.service";
|
||||||
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
||||||
import { UserKeyDefinition } from "../user-key-definition";
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
import { ActiveUserState, CombinedState, activeMarker } from "../user-state";
|
import { ActiveUserState, CombinedState, activeMarker } from "../user-state";
|
||||||
@@ -42,6 +43,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
protected keyDefinition: UserKeyDefinition<T>,
|
protected keyDefinition: UserKeyDefinition<T>,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private chosenStorageLocation: AbstractStorageService & ObservableStorageService,
|
private chosenStorageLocation: AbstractStorageService & ObservableStorageService,
|
||||||
|
private stateEventRegistrarService: StateEventRegistrarService,
|
||||||
) {
|
) {
|
||||||
this.activeUserId$ = this.accountService.activeAccount$.pipe(
|
this.activeUserId$ = this.accountService.activeAccount$.pipe(
|
||||||
// We only care about the UserId but we do want to know about no user as well.
|
// We only care about the UserId but we do want to know about no user as well.
|
||||||
@@ -150,6 +152,11 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
|
|
||||||
const newState = configureState(currentState, combinedDependencies);
|
const newState = configureState(currentState, combinedDependencies);
|
||||||
await this.saveToStorage(key, newState);
|
await this.saveToStorage(key, newState);
|
||||||
|
if (newState != null && currentState == null) {
|
||||||
|
// Only register this state as something clearable on the first time it saves something
|
||||||
|
// worth deleting. This is helpful in making sure there is less of a race to adding events.
|
||||||
|
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
|
||||||
|
}
|
||||||
return [userId, newState];
|
return [userId, newState];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import {
|
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "../../abstractions/storage.service";
|
|
||||||
import { GlobalState } from "../global-state";
|
import { GlobalState } from "../global-state";
|
||||||
import { GlobalStateProvider } from "../global-state.provider";
|
import { GlobalStateProvider } from "../global-state.provider";
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
|
||||||
|
|
||||||
import { DefaultGlobalState } from "./default-global-state";
|
import { DefaultGlobalState } from "./default-global-state";
|
||||||
|
|
||||||
export class DefaultGlobalStateProvider implements GlobalStateProvider {
|
export class DefaultGlobalStateProvider implements GlobalStateProvider {
|
||||||
private globalStateCache: Record<string, GlobalState<unknown>> = {};
|
private globalStateCache: Record<string, GlobalState<unknown>> = {};
|
||||||
|
|
||||||
constructor(
|
constructor(private storageServiceProvider: StorageServiceProvider) {}
|
||||||
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
|
||||||
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
||||||
const cacheKey = this.buildCacheKey(keyDefinition);
|
const [location, storageService] = this.storageServiceProvider.get(
|
||||||
|
keyDefinition.stateDefinition.defaultStorageLocation,
|
||||||
|
keyDefinition.stateDefinition.storageLocationOverrides,
|
||||||
|
);
|
||||||
|
const cacheKey = this.buildCacheKey(location, keyDefinition);
|
||||||
const existingGlobalState = this.globalStateCache[cacheKey];
|
const existingGlobalState = this.globalStateCache[cacheKey];
|
||||||
if (existingGlobalState != null) {
|
if (existingGlobalState != null) {
|
||||||
// The cast into the actual generic is safe because of rules around key definitions
|
// The cast into the actual generic is safe because of rules around key definitions
|
||||||
@@ -27,30 +23,13 @@ export class DefaultGlobalStateProvider implements GlobalStateProvider {
|
|||||||
return existingGlobalState as DefaultGlobalState<T>;
|
return existingGlobalState as DefaultGlobalState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGlobalState = new DefaultGlobalState<T>(
|
const newGlobalState = new DefaultGlobalState<T>(keyDefinition, storageService);
|
||||||
keyDefinition,
|
|
||||||
this.getLocation(keyDefinition.stateDefinition),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.globalStateCache[cacheKey] = newGlobalState;
|
this.globalStateCache[cacheKey] = newGlobalState;
|
||||||
return newGlobalState;
|
return newGlobalState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCacheKey(keyDefinition: KeyDefinition<unknown>) {
|
private buildCacheKey(location: string, keyDefinition: KeyDefinition<unknown>) {
|
||||||
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`;
|
return `${location}_${keyDefinition.fullName}`;
|
||||||
}
|
|
||||||
|
|
||||||
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
|
|
||||||
return keyDefinition.stateDefinition.defaultStorageLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getLocation(stateDefinition: StateDefinition) {
|
|
||||||
const location = stateDefinition.defaultStorageLocation;
|
|
||||||
switch (location) {
|
|
||||||
case "disk":
|
|
||||||
return this.diskStorage;
|
|
||||||
case "memory":
|
|
||||||
return this.memoryStorage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import {
|
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||||
AbstractMemoryStorageService,
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "../../abstractions/storage.service";
|
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
|
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
|
||||||
import { SingleUserState } from "../user-state";
|
import { SingleUserState } from "../user-state";
|
||||||
import { SingleUserStateProvider } from "../user-state.provider";
|
import { SingleUserStateProvider } from "../user-state.provider";
|
||||||
@@ -16,8 +12,8 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
private cache: Record<string, SingleUserState<unknown>> = {};
|
private cache: Record<string, SingleUserState<unknown>> = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
private readonly storageServiceProvider: StorageServiceProvider,
|
||||||
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
private readonly stateEventRegistrarService: StateEventRegistrarService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get<T>(
|
get<T>(
|
||||||
@@ -27,7 +23,11 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
if (!isUserKeyDefinition(keyDefinition)) {
|
if (!isUserKeyDefinition(keyDefinition)) {
|
||||||
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
}
|
}
|
||||||
const cacheKey = this.buildCacheKey(userId, keyDefinition);
|
const [location, storageService] = this.storageServiceProvider.get(
|
||||||
|
keyDefinition.stateDefinition.defaultStorageLocation,
|
||||||
|
keyDefinition.stateDefinition.storageLocationOverrides,
|
||||||
|
);
|
||||||
|
const cacheKey = this.buildCacheKey(location, userId, keyDefinition);
|
||||||
const existingUserState = this.cache[cacheKey];
|
const existingUserState = this.cache[cacheKey];
|
||||||
if (existingUserState != null) {
|
if (existingUserState != null) {
|
||||||
// I have to cast out of the unknown generic but this should be safe if rules
|
// I have to cast out of the unknown generic but this should be safe if rules
|
||||||
@@ -35,38 +35,21 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
return existingUserState as SingleUserState<T>;
|
return existingUserState as SingleUserState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUserState = this.buildSingleUserState(userId, keyDefinition);
|
const newUserState = new DefaultSingleUserState<T>(
|
||||||
|
userId,
|
||||||
|
keyDefinition,
|
||||||
|
storageService,
|
||||||
|
this.stateEventRegistrarService,
|
||||||
|
);
|
||||||
this.cache[cacheKey] = newUserState;
|
this.cache[cacheKey] = newUserState;
|
||||||
return newUserState;
|
return newUserState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCacheKey(userId: UserId, keyDefinition: UserKeyDefinition<unknown>) {
|
private buildCacheKey(
|
||||||
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}_${userId}`;
|
location: string,
|
||||||
}
|
|
||||||
|
|
||||||
protected buildSingleUserState<T>(
|
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
keyDefinition: UserKeyDefinition<T>,
|
keyDefinition: UserKeyDefinition<unknown>,
|
||||||
): SingleUserState<T> {
|
) {
|
||||||
return new DefaultSingleUserState<T>(
|
return `${location}_${keyDefinition.fullName}_${userId}`;
|
||||||
userId,
|
|
||||||
keyDefinition,
|
|
||||||
this.getLocation(keyDefinition.stateDefinition),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
|
||||||
return keyDefinition.stateDefinition.defaultStorageLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getLocation(stateDefinition: StateDefinition) {
|
|
||||||
// The default implementations don't support the client overrides
|
|
||||||
// it is up to the client to extend this class and add that support
|
|
||||||
switch (stateDefinition.defaultStorageLocation) {
|
|
||||||
case "disk":
|
|
||||||
return this.diskStorage;
|
|
||||||
case "memory":
|
|
||||||
return this.memoryStorage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* @jest-environment ../shared/test.environment.ts
|
* @jest-environment ../shared/test.environment.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom, of } from "rxjs";
|
import { firstValueFrom, of } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
|||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
import { UserKeyDefinition } from "../user-key-definition";
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
|
|
||||||
import { DefaultSingleUserState } from "./default-single-user-state";
|
import { DefaultSingleUserState } from "./default-single-user-state";
|
||||||
@@ -42,11 +44,17 @@ const userKey = testKeyDefinition.buildKey(userId);
|
|||||||
describe("DefaultSingleUserState", () => {
|
describe("DefaultSingleUserState", () => {
|
||||||
let diskStorageService: FakeStorageService;
|
let diskStorageService: FakeStorageService;
|
||||||
let userState: DefaultSingleUserState<TestState>;
|
let userState: DefaultSingleUserState<TestState>;
|
||||||
|
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
||||||
const newData = { date: new Date() };
|
const newData = { date: new Date() };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
diskStorageService = new FakeStorageService();
|
diskStorageService = new FakeStorageService();
|
||||||
userState = new DefaultSingleUserState(userId, testKeyDefinition, diskStorageService);
|
userState = new DefaultSingleUserState(
|
||||||
|
userId,
|
||||||
|
testKeyDefinition,
|
||||||
|
diskStorageService,
|
||||||
|
stateEventRegistrarService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -255,6 +263,49 @@ describe("DefaultSingleUserState", () => {
|
|||||||
expect(emissions).toHaveLength(2);
|
expect(emissions).toHaveLength(2);
|
||||||
expect(emissions).toEqual(expect.arrayContaining([initialState, newState]));
|
expect(emissions).toEqual(expect.arrayContaining([initialState, newState]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"should register user key definition when state transitions from null-ish (%s) to non-null",
|
||||||
|
async (startingValue: TestState | null) => {
|
||||||
|
const initialState: Record<string, TestState> = {};
|
||||||
|
initialState[userKey] = startingValue;
|
||||||
|
|
||||||
|
diskStorageService.internalUpdateStore(initialState);
|
||||||
|
|
||||||
|
await userState.update(() => ({ array: ["one"], date: new Date() }));
|
||||||
|
|
||||||
|
expect(stateEventRegistrarService.registerEvents).toHaveBeenCalledWith(testKeyDefinition);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should not register user key definition when state has preexisting value", async () => {
|
||||||
|
const initialState: Record<string, TestState> = {};
|
||||||
|
initialState[userKey] = {
|
||||||
|
date: new Date(2019, 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
diskStorageService.internalUpdateStore(initialState);
|
||||||
|
|
||||||
|
await userState.update(() => ({ array: ["one"], date: new Date() }));
|
||||||
|
|
||||||
|
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"should not register user key definition when setting value to null-ish (%s) value",
|
||||||
|
async (updatedValue: TestState | null) => {
|
||||||
|
const initialState: Record<string, TestState> = {};
|
||||||
|
initialState[userKey] = {
|
||||||
|
date: new Date(2019, 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
diskStorageService.internalUpdateStore(initialState);
|
||||||
|
|
||||||
|
await userState.update(() => updatedValue);
|
||||||
|
|
||||||
|
expect(stateEventRegistrarService.registerEvents).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("update races", () => {
|
describe("update races", () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "../../abstractions/storage.service";
|
} from "../../abstractions/storage.service";
|
||||||
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
||||||
import { UserKeyDefinition } from "../user-key-definition";
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
import { CombinedState, SingleUserState } from "../user-state";
|
import { CombinedState, SingleUserState } from "../user-state";
|
||||||
@@ -35,6 +36,7 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
|
|||||||
readonly userId: UserId,
|
readonly userId: UserId,
|
||||||
private keyDefinition: UserKeyDefinition<T>,
|
private keyDefinition: UserKeyDefinition<T>,
|
||||||
private chosenLocation: AbstractStorageService & ObservableStorageService,
|
private chosenLocation: AbstractStorageService & ObservableStorageService,
|
||||||
|
private stateEventRegistrarService: StateEventRegistrarService,
|
||||||
) {
|
) {
|
||||||
this.storageKey = this.keyDefinition.buildKey(this.userId);
|
this.storageKey = this.keyDefinition.buildKey(this.userId);
|
||||||
const initialStorageGet$ = defer(() => {
|
const initialStorageGet$ = defer(() => {
|
||||||
@@ -100,6 +102,11 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
|
|||||||
|
|
||||||
const newState = configureState(currentState, combinedDependencies);
|
const newState = configureState(currentState, combinedDependencies);
|
||||||
await this.chosenLocation.save(this.storageKey, newState);
|
await this.chosenLocation.save(this.storageKey, newState);
|
||||||
|
if (newState != null && currentState == null) {
|
||||||
|
// Only register this state as something clearable on the first time it saves something
|
||||||
|
// worth deleting. This is helpful in making sure there is less of a race to adding events.
|
||||||
|
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
|
||||||
|
}
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
import { mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
||||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
import { StateEventRegistrarService } from "../state-event-registrar.service";
|
||||||
|
|
||||||
import { DefaultActiveUserState } from "./default-active-user-state";
|
import { DefaultActiveUserState } from "./default-active-user-state";
|
||||||
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
|
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
|
||||||
@@ -12,6 +16,9 @@ import { DefaultSingleUserState } from "./default-single-user-state";
|
|||||||
import { DefaultSingleUserStateProvider } from "./default-single-user-state.provider";
|
import { DefaultSingleUserStateProvider } from "./default-single-user-state.provider";
|
||||||
|
|
||||||
describe("Specific State Providers", () => {
|
describe("Specific State Providers", () => {
|
||||||
|
const storageServiceProvider = mock<StorageServiceProvider>();
|
||||||
|
const stateEventRegistrarService = mock<StateEventRegistrarService>();
|
||||||
|
|
||||||
let singleSut: DefaultSingleUserStateProvider;
|
let singleSut: DefaultSingleUserStateProvider;
|
||||||
let activeSut: DefaultActiveUserStateProvider;
|
let activeSut: DefaultActiveUserStateProvider;
|
||||||
let globalSut: DefaultGlobalStateProvider;
|
let globalSut: DefaultGlobalStateProvider;
|
||||||
@@ -19,19 +26,20 @@ describe("Specific State Providers", () => {
|
|||||||
const fakeUser1 = "00000000-0000-1000-a000-000000000001" as UserId;
|
const fakeUser1 = "00000000-0000-1000-a000-000000000001" as UserId;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
storageServiceProvider.get.mockImplementation((location) => {
|
||||||
|
return [location, new FakeStorageService()];
|
||||||
|
});
|
||||||
|
|
||||||
singleSut = new DefaultSingleUserStateProvider(
|
singleSut = new DefaultSingleUserStateProvider(
|
||||||
new FakeStorageService() as any,
|
storageServiceProvider,
|
||||||
new FakeStorageService(),
|
stateEventRegistrarService,
|
||||||
);
|
);
|
||||||
activeSut = new DefaultActiveUserStateProvider(
|
activeSut = new DefaultActiveUserStateProvider(
|
||||||
mockAccountServiceWith(null),
|
mockAccountServiceWith(null),
|
||||||
new FakeStorageService() as any,
|
storageServiceProvider,
|
||||||
new FakeStorageService(),
|
stateEventRegistrarService,
|
||||||
);
|
|
||||||
globalSut = new DefaultGlobalStateProvider(
|
|
||||||
new FakeStorageService() as any,
|
|
||||||
new FakeStorageService(),
|
|
||||||
);
|
);
|
||||||
|
globalSut = new DefaultGlobalStateProvider(storageServiceProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fakeDiskStateDefinition = new StateDefinition("fake", "disk");
|
const fakeDiskStateDefinition = new StateDefinition("fake", "disk");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export { DerivedState } from "./derived-state";
|
|||||||
export { GlobalState } from "./global-state";
|
export { GlobalState } from "./global-state";
|
||||||
export { StateProvider } from "./state.provider";
|
export { StateProvider } from "./state.provider";
|
||||||
export { GlobalStateProvider } from "./global-state.provider";
|
export { GlobalStateProvider } from "./global-state.provider";
|
||||||
export { ActiveUserState, SingleUserState } from "./user-state";
|
export { ActiveUserState, SingleUserState, CombinedState } from "./user-state";
|
||||||
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
||||||
export { KeyDefinition } from "./key-definition";
|
export { KeyDefinition } from "./key-definition";
|
||||||
export { StateUpdateOptions } from "./state-update-options";
|
export { StateUpdateOptions } from "./state-update-options";
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
|
|||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
|
||||||
|
"userNotificationSettings",
|
||||||
|
"disk",
|
||||||
|
);
|
||||||
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
||||||
|
|
||||||
export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanner", "disk", {
|
export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanner", "disk", {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
|||||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateService } from "../../platform/abstractions/state.service";
|
||||||
import { Account } from "../../platform/models/domain/account";
|
import { Account } from "../../platform/models/domain/account";
|
||||||
|
import { StateEventRunnerService } from "../../platform/state";
|
||||||
import { CipherService } from "../../vault/abstractions/cipher.service";
|
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "../../vault/abstractions/collection.service";
|
import { CollectionService } from "../../vault/abstractions/collection.service";
|
||||||
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
|
||||||
@@ -28,6 +29,7 @@ describe("VaultTimeoutService", () => {
|
|||||||
let stateService: MockProxy<StateService>;
|
let stateService: MockProxy<StateService>;
|
||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||||
|
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
|
||||||
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
|
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
|
||||||
let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>;
|
let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>;
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ describe("VaultTimeoutService", () => {
|
|||||||
stateService = mock();
|
stateService = mock();
|
||||||
authService = mock();
|
authService = mock();
|
||||||
vaultTimeoutSettingsService = mock();
|
vaultTimeoutSettingsService = mock();
|
||||||
|
stateEventRunnerService = mock();
|
||||||
|
|
||||||
lockedCallback = jest.fn();
|
lockedCallback = jest.fn();
|
||||||
loggedOutCallback = jest.fn();
|
loggedOutCallback = jest.fn();
|
||||||
@@ -73,6 +76,7 @@ describe("VaultTimeoutService", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
authService,
|
authService,
|
||||||
vaultTimeoutSettingsService,
|
vaultTimeoutSettingsService,
|
||||||
|
stateEventRunnerService,
|
||||||
lockedCallback,
|
lockedCallback,
|
||||||
loggedOutCallback,
|
loggedOutCallback,
|
||||||
);
|
);
|
||||||
@@ -103,7 +107,8 @@ describe("VaultTimeoutService", () => {
|
|||||||
return Promise.resolve(accounts[userId]?.authStatus);
|
return Promise.resolve(accounts[userId]?.authStatus);
|
||||||
});
|
});
|
||||||
stateService.getIsAuthenticated.mockImplementation((options) => {
|
stateService.getIsAuthenticated.mockImplementation((options) => {
|
||||||
return Promise.resolve(accounts[options.userId]?.isAuthenticated);
|
// Just like actual state service, if no userId is given fallback to active userId
|
||||||
|
return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated);
|
||||||
});
|
});
|
||||||
|
|
||||||
vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => {
|
vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => {
|
||||||
@@ -337,4 +342,80 @@ describe("VaultTimeoutService", () => {
|
|||||||
expectNoAction("1");
|
expectNoAction("1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("lock", () => {
|
||||||
|
const setupLock = () => {
|
||||||
|
setupAccounts(
|
||||||
|
{
|
||||||
|
user1: {
|
||||||
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
|
isAuthenticated: true,
|
||||||
|
},
|
||||||
|
user2: {
|
||||||
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
|
isAuthenticated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "user1",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should call state event runner with currently active user if no user passed into lock", async () => {
|
||||||
|
setupLock();
|
||||||
|
|
||||||
|
await vaultTimeoutService.lock();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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(lockedCallback).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call state event runner with user passed into lock", async () => {
|
||||||
|
setupLock();
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: "user2" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call locked callback with user passed into lock", async () => {
|
||||||
|
setupLock();
|
||||||
|
|
||||||
|
await vaultTimeoutService.lock("user2");
|
||||||
|
|
||||||
|
expect(lockedCallback).toHaveBeenCalledWith("user2");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
|
|||||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateService } from "../../platform/abstractions/state.service";
|
||||||
|
import { StateEventRunnerService } from "../../platform/state";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { CipherService } from "../../vault/abstractions/cipher.service";
|
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "../../vault/abstractions/collection.service";
|
import { CollectionService } from "../../vault/abstractions/collection.service";
|
||||||
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
|
||||||
@@ -29,6 +31,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
|
private stateEventRunnerService: StateEventRunnerService,
|
||||||
private lockedCallback: (userId?: string) => Promise<void> = null,
|
private lockedCallback: (userId?: string) => Promise<void> = null,
|
||||||
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null,
|
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null,
|
||||||
) {}
|
) {}
|
||||||
@@ -81,7 +84,9 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
await this.logOut(userId);
|
await this.logOut(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId == null || userId === (await this.stateService.getUserId())) {
|
const currentUserId = await this.stateService.getUserId();
|
||||||
|
|
||||||
|
if (userId == null || userId === currentUserId) {
|
||||||
this.searchService.clearIndex();
|
this.searchService.clearIndex();
|
||||||
await this.folderService.clearCache();
|
await this.folderService.clearCache();
|
||||||
await this.collectionService.clearActiveUserCache();
|
await this.collectionService.clearActiveUserCache();
|
||||||
@@ -98,6 +103,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||||||
|
|
||||||
await this.cipherService.clearCache(userId);
|
await this.cipherService.clearCache(userId);
|
||||||
|
|
||||||
|
await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId);
|
||||||
|
|
||||||
|
// 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 });
|
this.messagingService.send("locked", { userId: userId });
|
||||||
|
|
||||||
if (this.lockedCallback != null) {
|
if (this.lockedCallback != null) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { ClearClipboardDelayMigrator } from "./migrations/25-move-clear-clipboar
|
|||||||
import { RevertLastSyncMigrator } from "./migrations/26-revert-move-last-sync-to-state-provider";
|
import { RevertLastSyncMigrator } from "./migrations/26-revert-move-last-sync-to-state-provider";
|
||||||
import { BadgeSettingsMigrator } from "./migrations/27-move-badge-settings-to-state-providers";
|
import { BadgeSettingsMigrator } from "./migrations/27-move-badge-settings-to-state-providers";
|
||||||
import { MoveBiometricUnlockToStateProviders } from "./migrations/28-move-biometric-unlock-to-state-providers";
|
import { MoveBiometricUnlockToStateProviders } from "./migrations/28-move-biometric-unlock-to-state-providers";
|
||||||
|
import { UserNotificationSettingsKeyMigrator } from "./migrations/29-move-user-notification-settings-to-state-provider";
|
||||||
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
import { FixPremiumMigrator } from "./migrations/3-fix-premium";
|
||||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
@@ -33,7 +34,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 2;
|
export const MIN_VERSION = 2;
|
||||||
export const CURRENT_VERSION = 28;
|
export const CURRENT_VERSION = 29;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@@ -64,7 +65,8 @@ export function createMigrationBuilder() {
|
|||||||
.with(ClearClipboardDelayMigrator, 24, 25)
|
.with(ClearClipboardDelayMigrator, 24, 25)
|
||||||
.with(RevertLastSyncMigrator, 25, 26)
|
.with(RevertLastSyncMigrator, 25, 26)
|
||||||
.with(BadgeSettingsMigrator, 26, 27)
|
.with(BadgeSettingsMigrator, 26, 27)
|
||||||
.with(MoveBiometricUnlockToStateProviders, 27, CURRENT_VERSION);
|
.with(MoveBiometricUnlockToStateProviders, 27, 28)
|
||||||
|
.with(UserNotificationSettingsKeyMigrator, 28, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { any, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { ProviderMigrator } from "./28-move-provider-state-to-state-provider";
|
||||||
|
|
||||||
|
function exampleProvider1() {
|
||||||
|
return JSON.stringify({
|
||||||
|
id: "id",
|
||||||
|
name: "name",
|
||||||
|
status: 0,
|
||||||
|
type: 0,
|
||||||
|
enabled: true,
|
||||||
|
useEvents: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2"],
|
||||||
|
"user-1": {
|
||||||
|
data: {
|
||||||
|
providers: {
|
||||||
|
"provider-id-1": exampleProvider1(),
|
||||||
|
"provider-id-2": {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
data: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
"user_user-1_providers_providers": {
|
||||||
|
"provider-id-1": exampleProvider1(),
|
||||||
|
"provider-id-2": {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user_user-2_providers_providers": null as any,
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2"],
|
||||||
|
"user-1": {
|
||||||
|
data: {
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
data: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ProviderMigrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: ProviderMigrator;
|
||||||
|
const keyDefinitionLike = {
|
||||||
|
key: "providers",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "providers",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 28);
|
||||||
|
sut = new ProviderMigrator(27, 28);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove providers from all accounts", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
data: {
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set providers value for each account", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
|
||||||
|
"provider-id-1": exampleProvider1(),
|
||||||
|
"provider-id-2": {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 27);
|
||||||
|
sut = new ProviderMigrator(27, 28);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add explicit value back to accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
data: {
|
||||||
|
providers: {
|
||||||
|
"provider-id-1": exampleProvider1(),
|
||||||
|
"provider-id-2": {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
otherStuff: "overStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not try to restore values to missing accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
enum ProviderUserStatusType {
|
||||||
|
Invited = 0,
|
||||||
|
Accepted = 1,
|
||||||
|
Confirmed = 2,
|
||||||
|
Revoked = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProviderUserType {
|
||||||
|
ProviderAdmin = 0,
|
||||||
|
ServiceUser = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderData = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: ProviderUserStatusType;
|
||||||
|
type: ProviderUserType;
|
||||||
|
enabled: boolean;
|
||||||
|
userId: string;
|
||||||
|
useEvents: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
data?: {
|
||||||
|
providers?: Record<string, Jsonify<ProviderData>>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const USER_PROVIDERS: KeyDefinitionLike = {
|
||||||
|
key: "providers",
|
||||||
|
stateDefinition: {
|
||||||
|
name: "providers",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ProviderMigrator extends Migrator<27, 28> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
const value = account?.data?.providers;
|
||||||
|
if (value != null) {
|
||||||
|
await helper.setToUser(userId, USER_PROVIDERS, value);
|
||||||
|
delete account.data.providers;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
const value = await helper.getFromUser(userId, USER_PROVIDERS);
|
||||||
|
if (account) {
|
||||||
|
account.data = Object.assign(account.data ?? {}, {
|
||||||
|
providers: value,
|
||||||
|
});
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
await helper.setToUser(userId, USER_PROVIDERS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(accounts.map(({ userId, account }) => rollbackAccount(userId, account)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { UserNotificationSettingsKeyMigrator } from "./29-move-user-notification-settings-to-state-provider";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
disableAddLoginNotification: false,
|
||||||
|
disableChangedPasswordNotification: false,
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
global_userNotificationSettings_enableAddedLoginPrompt: true,
|
||||||
|
global_userNotificationSettings_enableChangedPasswordPrompt: true,
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const userNotificationSettingsLocalStateDefinition: {
|
||||||
|
stateDefinition: StateDefinitionLike;
|
||||||
|
} = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "userNotificationSettings",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ProviderKeysMigrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: UserNotificationSettingsKeyMigrator;
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 28);
|
||||||
|
sut = new UserNotificationSettingsKeyMigrator(28, 29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove disableAddLoginNotification and disableChangedPasswordNotification global setting", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("global", { otherStuff: "otherStuff1" });
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("global", { otherStuff: "otherStuff1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set global user notification setting values", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToGlobal).toHaveBeenCalledTimes(2);
|
||||||
|
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||||
|
{ ...userNotificationSettingsLocalStateDefinition, key: "enableAddedLoginPrompt" },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||||
|
{ ...userNotificationSettingsLocalStateDefinition, key: "enableChangedPasswordPrompt" },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 29);
|
||||||
|
sut = new UserNotificationSettingsKeyMigrator(28, 29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should null out new global values", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToGlobal).toHaveBeenCalledTimes(2);
|
||||||
|
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||||
|
{ ...userNotificationSettingsLocalStateDefinition, key: "enableAddedLoginPrompt" },
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||||
|
{ ...userNotificationSettingsLocalStateDefinition, key: "enableChangedPasswordPrompt" },
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add explicit global values back", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledTimes(2);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||||
|
disableAddLoginNotification: false,
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
});
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||||
|
disableChangedPasswordNotification: false,
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedGlobalState = {
|
||||||
|
disableAddLoginNotification?: boolean;
|
||||||
|
disableChangedPasswordNotification?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UserNotificationSettingsKeyMigrator extends Migrator<28, 29> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const globalState = await helper.get<ExpectedGlobalState>("global");
|
||||||
|
|
||||||
|
// disableAddLoginNotification -> enableAddedLoginPrompt
|
||||||
|
if (globalState?.disableAddLoginNotification != null) {
|
||||||
|
await helper.setToGlobal(
|
||||||
|
{
|
||||||
|
stateDefinition: {
|
||||||
|
name: "userNotificationSettings",
|
||||||
|
},
|
||||||
|
key: "enableAddedLoginPrompt",
|
||||||
|
},
|
||||||
|
!globalState.disableAddLoginNotification,
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete `disableAddLoginNotification` from state global
|
||||||
|
delete globalState.disableAddLoginNotification;
|
||||||
|
|
||||||
|
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableChangedPasswordNotification -> enableChangedPasswordPrompt
|
||||||
|
if (globalState?.disableChangedPasswordNotification != null) {
|
||||||
|
await helper.setToGlobal(
|
||||||
|
{
|
||||||
|
stateDefinition: {
|
||||||
|
name: "userNotificationSettings",
|
||||||
|
},
|
||||||
|
key: "enableChangedPasswordPrompt",
|
||||||
|
},
|
||||||
|
!globalState.disableChangedPasswordNotification,
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete `disableChangedPasswordNotification` from state global
|
||||||
|
delete globalState.disableChangedPasswordNotification;
|
||||||
|
|
||||||
|
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
|
||||||
|
|
||||||
|
const enableAddedLoginPrompt: boolean = await helper.getFromGlobal({
|
||||||
|
stateDefinition: {
|
||||||
|
name: "userNotificationSettings",
|
||||||
|
},
|
||||||
|
key: "enableAddedLoginPrompt",
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableChangedPasswordPrompt: boolean = await helper.getFromGlobal({
|
||||||
|
stateDefinition: {
|
||||||
|
name: "userNotificationSettings",
|
||||||
|
},
|
||||||
|
key: "enableChangedPasswordPrompt",
|
||||||
|
});
|
||||||
|
|
||||||
|
// enableAddedLoginPrompt -> disableAddLoginNotification
|
||||||
|
if (enableAddedLoginPrompt) {
|
||||||
|
await helper.set<ExpectedGlobalState>("global", {
|
||||||
|
...globalState,
|
||||||
|
disableAddLoginNotification: !enableAddedLoginPrompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove the global state provider framework key for `enableAddedLoginPrompt`
|
||||||
|
await helper.setToGlobal(
|
||||||
|
{
|
||||||
|
stateDefinition: {
|
||||||
|
name: "userNotificationSettings",
|
||||||
|
},
|
||||||
|
key: "enableAddedLoginPrompt",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableChangedPasswordPrompt -> disableChangedPasswordNotification
|
||||||
|
if (enableChangedPasswordPrompt) {
|
||||||
|
await helper.set<ExpectedGlobalState>("global", {
|
||||||
|
...globalState,
|
||||||
|
disableChangedPasswordNotification: !enableChangedPasswordPrompt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove the global state provider framework key for `enableChangedPasswordPrompt`
|
||||||
|
await helper.setToGlobal(
|
||||||
|
{
|
||||||
|
stateDefinition: {
|
||||||
|
name: "userNotificationSettings",
|
||||||
|
},
|
||||||
|
key: "enableChangedPasswordPrompt",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,18 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy as AdminPolicy } from "../../../admin-console/models/domain/policy";
|
import { Policy as AdminPolicy } from "../../../admin-console/models/domain/policy";
|
||||||
import { KeyDefinition } from "../../../platform/state";
|
import { SingleUserState } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
|
|
||||||
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||||
|
|
||||||
/** Tailors the generator service to generate a specific kind of credentials */
|
/** Tailors the generator service to generate a specific kind of credentials */
|
||||||
export abstract class GeneratorStrategy<Options, Policy> {
|
export abstract class GeneratorStrategy<Options, Policy> {
|
||||||
/** The key used when storing credentials on disk. */
|
/** Retrieve application state that persists across locks.
|
||||||
disk: KeyDefinition<Options>;
|
* @param userId: identifies the user state to retrieve
|
||||||
|
* @returns the strategy's durable user state
|
||||||
|
*/
|
||||||
|
durableState: (userId: UserId) => SingleUserState<Options>;
|
||||||
|
|
||||||
/** Identifies the policy enforced by the generator. */
|
/** Identifies the policy enforced by the generator. */
|
||||||
policy: PolicyType;
|
policy: PolicyType;
|
||||||
@@ -19,7 +23,8 @@ export abstract class GeneratorStrategy<Options, Policy> {
|
|||||||
|
|
||||||
/** Creates an evaluator from a generator policy.
|
/** Creates an evaluator from a generator policy.
|
||||||
* @param policy The policy being evaluated.
|
* @param policy The policy being evaluated.
|
||||||
* @returns the policy evaluator.
|
* @returns the policy evaluator. If `policy` is is `null` or `undefined`,
|
||||||
|
* then the evaluator defaults to the application's limits.
|
||||||
* @throws when the policy's type does not match the generator's policy type.
|
* @throws when the policy's type does not match the generator's policy type.
|
||||||
*/
|
*/
|
||||||
evaluator: (policy: AdminPolicy) => PolicyEvaluator<Policy, Options>;
|
evaluator: (policy: AdminPolicy) => PolicyEvaluator<Policy, Options>;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
|
|
||||||
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||||
|
|
||||||
/** Generates credentials used for user authentication
|
/** Generates credentials used for user authentication
|
||||||
@@ -9,19 +11,22 @@ import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
|||||||
export abstract class GeneratorService<Options, Policy> {
|
export abstract class GeneratorService<Options, Policy> {
|
||||||
/** An observable monitoring the options saved to disk.
|
/** An observable monitoring the options saved to disk.
|
||||||
* The observable updates when the options are saved.
|
* The observable updates when the options are saved.
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
*/
|
*/
|
||||||
options$: Observable<Options>;
|
options$: (userId: UserId) => Observable<Options>;
|
||||||
|
|
||||||
/** An observable monitoring the options used to enforce policy.
|
/** An observable monitoring the options used to enforce policy.
|
||||||
* The observable updates when the policy changes.
|
* The observable updates when the policy changes.
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
*/
|
*/
|
||||||
policy$: Observable<PolicyEvaluator<Policy, Options>>;
|
evaluator$: (userId: UserId) => Observable<PolicyEvaluator<Policy, Options>>;
|
||||||
|
|
||||||
/** Enforces the policy on the given options
|
/** Enforces the policy on the given options
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
* @param options the options to enforce the policy on
|
* @param options the options to enforce the policy on
|
||||||
* @returns a new instance of the options with the policy enforced
|
* @returns a new instance of the options with the policy enforced
|
||||||
*/
|
*/
|
||||||
enforcePolicy: (options: Options) => Promise<Options>;
|
enforcePolicy: (userId: UserId, options: Options) => Promise<Options>;
|
||||||
|
|
||||||
/** Generates credentials
|
/** Generates credentials
|
||||||
* @param options the options to generate credentials with
|
* @param options the options to generate credentials with
|
||||||
@@ -30,8 +35,9 @@ export abstract class GeneratorService<Options, Policy> {
|
|||||||
generate: (options: Options) => Promise<string>;
|
generate: (options: Options) => Promise<string>;
|
||||||
|
|
||||||
/** Saves the given options to disk.
|
/** Saves the given options to disk.
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
* @param options the options to save
|
* @param options the options to save
|
||||||
* @returns a promise that resolves when the options are saved
|
* @returns a promise that resolves when the options are saved
|
||||||
*/
|
*/
|
||||||
saveOptions: (options: Options) => Promise<void>;
|
saveOptions: (userId: UserId, options: Options) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,42 +6,45 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { FakeActiveUserStateProvider, mockAccountServiceWith } from "../../../spec";
|
import { FakeSingleUserState, awaitAsync } from "../../../spec";
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "../../admin-console/enums";
|
import { PolicyType } from "../../admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../admin-console/models/domain/policy";
|
import { Policy } from "../../admin-console/models/domain/policy";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { SingleUserState } from "../../platform/state";
|
||||||
import { ActiveUserState, ActiveUserStateProvider, KeyDefinition } from "../../platform/state";
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
import { GeneratorStrategy, PolicyEvaluator } from "./abstractions";
|
import { GeneratorStrategy, PolicyEvaluator } from "./abstractions";
|
||||||
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "./key-definitions";
|
|
||||||
import { PasswordGenerationOptions } from "./password";
|
import { PasswordGenerationOptions } from "./password";
|
||||||
|
|
||||||
import { DefaultGeneratorService } from ".";
|
import { DefaultGeneratorService } from ".";
|
||||||
|
|
||||||
function mockPolicyService(config?: { data?: any; policy?: BehaviorSubject<Policy> }) {
|
function mockPolicyService(config?: { state?: BehaviorSubject<Policy> }) {
|
||||||
const state = mock<Policy>({ data: config?.data ?? {} });
|
|
||||||
const subject = config?.policy ?? new BehaviorSubject<Policy>(state);
|
|
||||||
|
|
||||||
const service = mock<PolicyService>();
|
const service = mock<PolicyService>();
|
||||||
service.get$.mockReturnValue(subject.asObservable());
|
|
||||||
|
// FIXME: swap out the mock return value when `getAll$` becomes available
|
||||||
|
const stateValue = config?.state ?? new BehaviorSubject<Policy>(null);
|
||||||
|
service.get$.mockReturnValue(stateValue);
|
||||||
|
|
||||||
|
// const stateValue = config?.state ?? new BehaviorSubject<Policy[]>(null);
|
||||||
|
// service.getAll$.mockReturnValue(stateValue);
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockGeneratorStrategy(config?: {
|
function mockGeneratorStrategy(config?: {
|
||||||
disk?: KeyDefinition<any>;
|
userState?: SingleUserState<any>;
|
||||||
policy?: PolicyType;
|
policy?: PolicyType;
|
||||||
evaluator?: any;
|
evaluator?: any;
|
||||||
}) {
|
}) {
|
||||||
|
const durableState =
|
||||||
|
config?.userState ?? new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
||||||
const strategy = mock<GeneratorStrategy<any, any>>({
|
const strategy = mock<GeneratorStrategy<any, any>>({
|
||||||
// intentionally arbitrary so that tests that need to check
|
// intentionally arbitrary so that tests that need to check
|
||||||
// whether they're used properly are guaranteed to test
|
// whether they're used properly are guaranteed to test
|
||||||
// the value from `config`.
|
// the value from `config`.
|
||||||
disk: config?.disk ?? {},
|
durableState: jest.fn(() => durableState),
|
||||||
policy: config?.policy ?? PolicyType.DisableSend,
|
policy: config?.policy ?? PolicyType.DisableSend,
|
||||||
evaluator: jest.fn(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>()),
|
evaluator: jest.fn(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>()),
|
||||||
});
|
});
|
||||||
@@ -49,129 +52,123 @@ function mockGeneratorStrategy(config?: {
|
|||||||
return strategy;
|
return strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Use the fake instead, once it's updated to monitor its method calls.
|
const SomeUser = "some user" as UserId;
|
||||||
function mockStateProvider(): [
|
const AnotherUser = "another user" as UserId;
|
||||||
ActiveUserStateProvider,
|
|
||||||
ActiveUserState<PasswordGenerationOptions>,
|
|
||||||
] {
|
|
||||||
const state = mock<ActiveUserState<PasswordGenerationOptions>>();
|
|
||||||
const provider = mock<ActiveUserStateProvider>();
|
|
||||||
provider.get.mockReturnValue(state);
|
|
||||||
|
|
||||||
return [provider, state];
|
|
||||||
}
|
|
||||||
|
|
||||||
function fakeStateProvider(key: KeyDefinition<any>, initalValue: any): FakeActiveUserStateProvider {
|
|
||||||
const userId = Utils.newGuid() as UserId;
|
|
||||||
const acctService = mockAccountServiceWith(userId);
|
|
||||||
const provider = new FakeActiveUserStateProvider(acctService);
|
|
||||||
provider.mockFor(key.key, initalValue);
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Password generator service", () => {
|
describe("Password generator service", () => {
|
||||||
describe("constructor()", () => {
|
|
||||||
it("should initialize the password generator policy", () => {
|
|
||||||
const policy = mockPolicyService();
|
|
||||||
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
|
||||||
|
|
||||||
new DefaultGeneratorService(strategy, policy, null);
|
|
||||||
|
|
||||||
expect(policy.get$).toHaveBeenCalledWith(PolicyType.PasswordGenerator);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("options$", () => {
|
describe("options$", () => {
|
||||||
it("should return the state from strategy.key", () => {
|
it("should retrieve durable state from the service", () => {
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const strategy = mockGeneratorStrategy({ disk: PASSPHRASE_SETTINGS });
|
const userState = new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
||||||
const [state] = mockStateProvider();
|
const strategy = mockGeneratorStrategy({ userState });
|
||||||
const service = new DefaultGeneratorService(strategy, policy, state);
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
// invoke the getter. It returns the state but that's not important.
|
const result = service.options$(SomeUser);
|
||||||
service.options$;
|
|
||||||
|
|
||||||
expect(state.get).toHaveBeenCalledWith(PASSPHRASE_SETTINGS);
|
expect(strategy.durableState).toHaveBeenCalledWith(SomeUser);
|
||||||
|
expect(result).toBe(userState.state$);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("saveOptions()", () => {
|
describe("saveOptions()", () => {
|
||||||
it("should update the state at strategy.key", async () => {
|
|
||||||
const policy = mockPolicyService();
|
|
||||||
const [provider, state] = mockStateProvider();
|
|
||||||
const strategy = mockGeneratorStrategy({ disk: PASSWORD_SETTINGS });
|
|
||||||
const service = new DefaultGeneratorService(strategy, policy, provider);
|
|
||||||
|
|
||||||
await service.saveOptions({});
|
|
||||||
|
|
||||||
expect(provider.get).toHaveBeenCalledWith(PASSWORD_SETTINGS);
|
|
||||||
expect(state.update).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should trigger an options$ update", async () => {
|
it("should trigger an options$ update", async () => {
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const strategy = mockGeneratorStrategy();
|
const userState = new FakeSingleUserState<PasswordGenerationOptions>(SomeUser, { length: 9 });
|
||||||
// using the fake here because we're testing that the update and the
|
const strategy = mockGeneratorStrategy({ userState });
|
||||||
// property are wired together. If we were to mock that, we'd be testing
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
// the mock configuration instead of the wiring.
|
|
||||||
const provider = fakeStateProvider(strategy.disk, { length: 9 });
|
|
||||||
const service = new DefaultGeneratorService(strategy, policy, provider);
|
|
||||||
|
|
||||||
await service.saveOptions({ length: 10 });
|
await service.saveOptions(SomeUser, { length: 10 });
|
||||||
|
await awaitAsync();
|
||||||
|
const options = await firstValueFrom(service.options$(SomeUser));
|
||||||
|
|
||||||
const options = await firstValueFrom(service.options$);
|
expect(strategy.durableState).toHaveBeenCalledWith(SomeUser);
|
||||||
expect(options).toEqual({ length: 10 });
|
expect(options).toEqual({ length: 10 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("policy$", () => {
|
describe("evaluator$", () => {
|
||||||
|
it("should initialize the password generator policy", async () => {
|
||||||
|
const policy = mockPolicyService();
|
||||||
|
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
|
await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
|
|
||||||
|
// FIXME: swap out the expect when `getAll$` becomes available
|
||||||
|
expect(policy.get$).toHaveBeenCalledWith(PolicyType.PasswordGenerator);
|
||||||
|
//expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||||
|
});
|
||||||
|
|
||||||
it("should map the policy using the generation strategy", async () => {
|
it("should map the policy using the generation strategy", async () => {
|
||||||
const policyService = mockPolicyService();
|
const policyService = mockPolicyService();
|
||||||
const evaluator = mock<PolicyEvaluator<any, any>>();
|
const evaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
const strategy = mockGeneratorStrategy({ evaluator });
|
const strategy = mockGeneratorStrategy({ evaluator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policyService);
|
||||||
|
|
||||||
const service = new DefaultGeneratorService(strategy, policyService, null);
|
const policy = await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
|
|
||||||
const policy = await firstValueFrom(service.policy$);
|
|
||||||
|
|
||||||
expect(policy).toBe(evaluator);
|
expect(policy).toBe(evaluator);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update the evaluator when the password generator policy changes", async () => {
|
||||||
|
// set up dependencies
|
||||||
|
const state = new BehaviorSubject<Policy>(null);
|
||||||
|
const policy = mockPolicyService({ state });
|
||||||
|
const strategy = mockGeneratorStrategy();
|
||||||
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
|
// model responses for the observable update
|
||||||
|
const firstEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
|
strategy.evaluator.mockReturnValueOnce(firstEvaluator);
|
||||||
|
const secondEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
|
strategy.evaluator.mockReturnValueOnce(secondEvaluator);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const evaluator$ = service.evaluator$(SomeUser);
|
||||||
|
const firstResult = await firstValueFrom(evaluator$);
|
||||||
|
state.next(null);
|
||||||
|
const secondResult = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(firstResult).toBe(firstEvaluator);
|
||||||
|
expect(secondResult).toBe(secondEvaluator);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cache the password generator policy", async () => {
|
||||||
|
const policy = mockPolicyService();
|
||||||
|
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
|
await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
|
await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
|
|
||||||
|
// FIXME: swap out the expect when `getAll$` becomes available
|
||||||
|
expect(policy.get$).toHaveBeenCalledTimes(1);
|
||||||
|
//expect(policy.getAll$).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cache the password generator policy for each user", async () => {
|
||||||
|
const policy = mockPolicyService();
|
||||||
|
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
|
await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
|
await firstValueFrom(service.evaluator$(AnotherUser));
|
||||||
|
|
||||||
|
// FIXME: enable this test when `getAll$` becomes available
|
||||||
|
// expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser);
|
||||||
|
// expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("enforcePolicy()", () => {
|
describe("enforcePolicy()", () => {
|
||||||
describe("should load the policy", () => {
|
|
||||||
it("from the cache by default", async () => {
|
|
||||||
const policy = mockPolicyService();
|
|
||||||
const strategy = mockGeneratorStrategy();
|
|
||||||
const service = new DefaultGeneratorService(strategy, policy, null);
|
|
||||||
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
|
|
||||||
expect(strategy.evaluator).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("from the policy service when the policy changes", async () => {
|
|
||||||
const policy = new BehaviorSubject<Policy>(mock<Policy>({ data: {} }));
|
|
||||||
const policyService = mockPolicyService({ policy });
|
|
||||||
const strategy = mockGeneratorStrategy();
|
|
||||||
const service = new DefaultGeneratorService(strategy, policyService, null);
|
|
||||||
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
policy.next(mock<Policy>({ data: { some: "change" } }));
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
|
|
||||||
expect(strategy.evaluator).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should evaluate the policy using the generation strategy", async () => {
|
it("should evaluate the policy using the generation strategy", async () => {
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const evaluator = mock<PolicyEvaluator<any, any>>();
|
const evaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
const strategy = mockGeneratorStrategy({ evaluator });
|
const strategy = mockGeneratorStrategy({ evaluator });
|
||||||
const service = new DefaultGeneratorService(strategy, policy, null);
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
await service.enforcePolicy({});
|
await service.enforcePolicy(SomeUser, {});
|
||||||
|
|
||||||
expect(evaluator.applyPolicy).toHaveBeenCalled();
|
expect(evaluator.applyPolicy).toHaveBeenCalled();
|
||||||
expect(evaluator.sanitize).toHaveBeenCalled();
|
expect(evaluator.sanitize).toHaveBeenCalled();
|
||||||
@@ -182,7 +179,7 @@ describe("Password generator service", () => {
|
|||||||
it("should invoke the generation strategy", async () => {
|
it("should invoke the generation strategy", async () => {
|
||||||
const strategy = mockGeneratorStrategy();
|
const strategy = mockGeneratorStrategy();
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const service = new DefaultGeneratorService(strategy, policy, null);
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
await service.generate({});
|
await service.generate({});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { firstValueFrom, map, share, timer, ReplaySubject, Observable } from "rx
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { ActiveUserStateProvider } from "../../platform/state";
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
import { GeneratorStrategy, GeneratorService, PolicyEvaluator } from "./abstractions";
|
import { GeneratorStrategy, GeneratorService, PolicyEvaluator } from "./abstractions";
|
||||||
|
|
||||||
@@ -13,45 +13,57 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
|||||||
* @param strategy tailors the service to a specific generator type
|
* @param strategy tailors the service to a specific generator type
|
||||||
* (e.g. password, passphrase)
|
* (e.g. password, passphrase)
|
||||||
* @param policy provides the policy to enforce
|
* @param policy provides the policy to enforce
|
||||||
* @param state saves and loads password generation options to the location
|
|
||||||
* specified by the strategy
|
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private strategy: GeneratorStrategy<Options, Policy>,
|
private strategy: GeneratorStrategy<Options, Policy>,
|
||||||
private policy: PolicyService,
|
private policy: PolicyService,
|
||||||
private state: ActiveUserStateProvider,
|
) {}
|
||||||
) {
|
|
||||||
this._policy$ = this.policy.get$(this.strategy.policy).pipe(
|
private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>();
|
||||||
|
|
||||||
|
/** {@link GeneratorService.options$()} */
|
||||||
|
options$(userId: UserId) {
|
||||||
|
return this.strategy.durableState(userId).state$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorService.saveOptions} */
|
||||||
|
async saveOptions(userId: UserId, options: Options): Promise<void> {
|
||||||
|
await this.strategy.durableState(userId).update(() => options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorService.evaluator$()} */
|
||||||
|
evaluator$(userId: UserId) {
|
||||||
|
let evaluator$ = this._evaluators$.get(userId);
|
||||||
|
|
||||||
|
if (!evaluator$) {
|
||||||
|
evaluator$ = this.createEvaluator(userId);
|
||||||
|
this._evaluators$.set(userId, evaluator$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluator$;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createEvaluator(userId: UserId) {
|
||||||
|
// FIXME: when it becomes possible to get a user-specific policy observable
|
||||||
|
// (`getAll$`) update this code to call it instead of `get$`.
|
||||||
|
const policies$ = this.policy.get$(this.strategy.policy);
|
||||||
|
|
||||||
|
// cache evaluator in a replay subject to amortize creation cost
|
||||||
|
// and reduce GC pressure.
|
||||||
|
const evaluator$ = policies$.pipe(
|
||||||
map((policy) => this.strategy.evaluator(policy)),
|
map((policy) => this.strategy.evaluator(policy)),
|
||||||
share({
|
share({
|
||||||
// cache evaluator in a replay subject to amortize creation cost
|
|
||||||
// and reduce GC pressure.
|
|
||||||
connector: () => new ReplaySubject(1),
|
connector: () => new ReplaySubject(1),
|
||||||
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return evaluator$;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _policy$: Observable<PolicyEvaluator<Policy, Options>>;
|
/** {@link GeneratorService.enforcePolicy()} */
|
||||||
|
async enforcePolicy(userId: UserId, options: Options): Promise<Options> {
|
||||||
/** {@link GeneratorService.options$} */
|
const policy = await firstValueFrom(this.evaluator$(userId));
|
||||||
get options$() {
|
|
||||||
return this.state.get(this.strategy.disk).state$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorService.saveOptions} */
|
|
||||||
async saveOptions(options: Options): Promise<void> {
|
|
||||||
await this.state.get(this.strategy.disk).update(() => options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorService.policy$} */
|
|
||||||
get policy$() {
|
|
||||||
return this._policy$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorService.enforcePolicy} */
|
|
||||||
async enforcePolicy(options: Options): Promise<Options> {
|
|
||||||
const policy = await firstValueFrom(this._policy$);
|
|
||||||
const evaluated = policy.applyPolicy(options);
|
const evaluated = policy.applyPolicy(options);
|
||||||
const sanitized = policy.sanitize(evaluated);
|
const sanitized = policy.sanitize(evaluated);
|
||||||
return sanitized;
|
return sanitized;
|
||||||
|
|||||||
@@ -9,15 +9,21 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||||
|
|
||||||
|
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
||||||
|
|
||||||
import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from ".";
|
import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Password generation strategy", () => {
|
describe("Password generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new PassphraseGeneratorStrategy(null);
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@@ -26,7 +32,7 @@ describe("Password generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new PassphraseGeneratorStrategy(null);
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@@ -45,21 +51,32 @@ describe("Password generation strategy", () => {
|
|||||||
includeNumber: true,
|
includeNumber: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map `null` to a default policy evaluator", () => {
|
||||||
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
|
const evaluator = strategy.evaluator(null);
|
||||||
|
|
||||||
|
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||||
|
expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(PASSPHRASE_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, PASSPHRASE_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -68,7 +85,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@@ -77,7 +94,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "passphrase",
|
type: "passphrase",
|
||||||
minNumberWords: 1,
|
minNumberWords: 1,
|
||||||
@@ -92,7 +109,7 @@ describe("Password generation strategy", () => {
|
|||||||
|
|
||||||
it("should set the generation type to passphrase", async () => {
|
it("should set the generation type to passphrase", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
await strategy.generate({ type: "foo" } as any);
|
await strategy.generate({ type: "foo" } as any);
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||||
|
|
||||||
import { PassphraseGenerationOptions } from "./passphrase-generation-options";
|
import { PassphraseGenerationOptions } from "./passphrase-generation-options";
|
||||||
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||||
import { PassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
import {
|
||||||
|
DisabledPassphraseGeneratorPolicy,
|
||||||
|
PassphraseGeneratorPolicy,
|
||||||
|
} from "./passphrase-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
@@ -19,11 +24,14 @@ export class PassphraseGeneratorStrategy
|
|||||||
/** instantiates the password generator strategy.
|
/** instantiates the password generator strategy.
|
||||||
* @param legacy generates the passphrase
|
* @param legacy generates the passphrase
|
||||||
*/
|
*/
|
||||||
constructor(private legacy: PasswordGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private legacy: PasswordGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return PASSPHRASE_SETTINGS;
|
return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@@ -37,6 +45,10 @@ export class PassphraseGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator {
|
evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator {
|
||||||
|
if (!policy) {
|
||||||
|
return new PassphraseGeneratorOptionsEvaluator(DisabledPassphraseGeneratorPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
|||||||
@@ -9,18 +9,24 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
|
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PasswordGenerationServiceAbstraction,
|
PasswordGenerationServiceAbstraction,
|
||||||
PasswordGeneratorOptionsEvaluator,
|
PasswordGeneratorOptionsEvaluator,
|
||||||
PasswordGeneratorStrategy,
|
PasswordGeneratorStrategy,
|
||||||
} from ".";
|
} from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Password generation strategy", () => {
|
describe("Password generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new PasswordGeneratorStrategy(null);
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@@ -29,7 +35,7 @@ describe("Password generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new PasswordGeneratorStrategy(null);
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@@ -56,21 +62,32 @@ describe("Password generation strategy", () => {
|
|||||||
specialCount: 1,
|
specialCount: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map `null` to a default policy evaluator", () => {
|
||||||
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
|
const evaluator = strategy.evaluator(null);
|
||||||
|
|
||||||
|
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||||
|
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(PASSWORD_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, PASSWORD_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -79,7 +96,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@@ -88,7 +105,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "password",
|
type: "password",
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
@@ -107,7 +124,7 @@ describe("Password generation strategy", () => {
|
|||||||
|
|
||||||
it("should set the generation type to password", async () => {
|
it("should set the generation type to password", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
await strategy.generate({ type: "foo" } as any);
|
await strategy.generate({ type: "foo" } as any);
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { PasswordGenerationOptions } from "./password-generation-options";
|
import { PasswordGenerationOptions } from "./password-generation-options";
|
||||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||||
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||||
import { PasswordGeneratorPolicy } from "./password-generator-policy";
|
import {
|
||||||
|
DisabledPasswordGeneratorPolicy,
|
||||||
|
PasswordGeneratorPolicy,
|
||||||
|
} from "./password-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
@@ -19,11 +24,14 @@ export class PasswordGeneratorStrategy
|
|||||||
/** instantiates the password generator strategy.
|
/** instantiates the password generator strategy.
|
||||||
* @param legacy generates the password
|
* @param legacy generates the password
|
||||||
*/
|
*/
|
||||||
constructor(private legacy: PasswordGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private legacy: PasswordGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return PASSWORD_SETTINGS;
|
return this.stateProvider.getUser(id, PASSWORD_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@@ -37,6 +45,10 @@ export class PasswordGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
|
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
|
||||||
|
if (!policy) {
|
||||||
|
return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
|||||||
@@ -83,7 +83,16 @@ describe("UserEncryptor", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("instance", () => {
|
describe("instance", () => {
|
||||||
it("gets a set value", async () => {
|
it("userId outputs the user input during construction", async () => {
|
||||||
|
const provider = await fakeStateProvider();
|
||||||
|
const encryptor = mockEncryptor();
|
||||||
|
|
||||||
|
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
||||||
|
|
||||||
|
expect(state.userId).toEqual(SomeUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("state$ gets a set value", async () => {
|
||||||
const provider = await fakeStateProvider();
|
const provider = await fakeStateProvider();
|
||||||
const encryptor = mockEncryptor();
|
const encryptor = mockEncryptor();
|
||||||
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
||||||
@@ -96,6 +105,20 @@ describe("UserEncryptor", () => {
|
|||||||
expect(result).toEqual(value);
|
expect(result).toEqual(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("combinedState$ gets a set value with the userId", async () => {
|
||||||
|
const provider = await fakeStateProvider();
|
||||||
|
const encryptor = mockEncryptor();
|
||||||
|
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
||||||
|
const value = { foo: true, bar: false };
|
||||||
|
|
||||||
|
await state.update(() => value);
|
||||||
|
await awaitAsync();
|
||||||
|
const [userId, result] = await firstValueFrom(state.combinedState$);
|
||||||
|
|
||||||
|
expect(result).toEqual(value);
|
||||||
|
expect(userId).toEqual(SomeUser);
|
||||||
|
});
|
||||||
|
|
||||||
it("round-trips json-serializable values", async () => {
|
it("round-trips json-serializable values", async () => {
|
||||||
const provider = await fakeStateProvider();
|
const provider = await fakeStateProvider();
|
||||||
const encryptor = mockEncryptor();
|
const encryptor = mockEncryptor();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Observable, concatMap, of, zip } from "rxjs";
|
import { Observable, concatMap, of, zip, map } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
SingleUserState,
|
SingleUserState,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
StateUpdateOptions,
|
StateUpdateOptions,
|
||||||
|
CombinedState,
|
||||||
} from "../../../platform/state";
|
} from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
|
||||||
@@ -37,7 +38,9 @@ type ClassifiedFormat<Disclosed> = {
|
|||||||
*
|
*
|
||||||
* DO NOT USE THIS for synchronized data.
|
* DO NOT USE THIS for synchronized data.
|
||||||
*/
|
*/
|
||||||
export class SecretState<Plaintext extends object, Disclosed> {
|
export class SecretState<Plaintext extends object, Disclosed>
|
||||||
|
implements SingleUserState<Plaintext>
|
||||||
|
{
|
||||||
// The constructor is private to avoid creating a circular dependency when
|
// The constructor is private to avoid creating a circular dependency when
|
||||||
// wiring the derived and secret states together.
|
// wiring the derived and secret states together.
|
||||||
private constructor(
|
private constructor(
|
||||||
@@ -46,8 +49,23 @@ export class SecretState<Plaintext extends object, Disclosed> {
|
|||||||
private readonly plaintext: DerivedState<Plaintext>,
|
private readonly plaintext: DerivedState<Plaintext>,
|
||||||
) {
|
) {
|
||||||
this.state$ = plaintext.state$;
|
this.state$ = plaintext.state$;
|
||||||
|
this.combinedState$ = plaintext.state$.pipe(map((state) => [this.encrypted.userId, state]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link SingleUserState.userId} */
|
||||||
|
get userId() {
|
||||||
|
return this.encrypted.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Observes changes to the decrypted secret state. The observer
|
||||||
|
* updates after the secret has been recorded to state storage.
|
||||||
|
* @returns `undefined` when the account is locked.
|
||||||
|
*/
|
||||||
|
readonly state$: Observable<Plaintext>;
|
||||||
|
|
||||||
|
/** {@link SingleUserState.combinedState$} */
|
||||||
|
readonly combinedState$: Observable<CombinedState<Plaintext>>;
|
||||||
|
|
||||||
/** Creates a secret state bound to an account encryptor. The account must be unlocked
|
/** Creates a secret state bound to an account encryptor. The account must be unlocked
|
||||||
* when this method is called.
|
* when this method is called.
|
||||||
* @param userId: the user to which the secret state is bound.
|
* @param userId: the user to which the secret state is bound.
|
||||||
@@ -106,12 +124,6 @@ export class SecretState<Plaintext extends object, Disclosed> {
|
|||||||
return secretState;
|
return secretState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Observes changes to the decrypted secret state. The observer
|
|
||||||
* updates after the secret has been recorded to state storage.
|
|
||||||
* @returns `undefined` when the account is locked.
|
|
||||||
*/
|
|
||||||
readonly state$: Observable<Plaintext>;
|
|
||||||
|
|
||||||
/** Updates the secret stored by this state.
|
/** Updates the secret stored by this state.
|
||||||
* @param configureState a callback that returns an updated decrypted
|
* @param configureState a callback that returns an updated decrypted
|
||||||
* secret state. The callback receives the state's present value as its
|
* secret state. The callback receives the state's present value as its
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Email subaddress list generation strategy", () => {
|
describe("Email subaddress list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new CatchallGeneratorStrategy(null);
|
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@@ -21,7 +25,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new CatchallGeneratorStrategy(null);
|
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@@ -34,21 +38,31 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({});
|
expect(evaluator.policy).toMatchObject({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map `null` to a default policy evaluator", () => {
|
||||||
|
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||||
|
const evaluator = strategy.evaluator(null);
|
||||||
|
|
||||||
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(CATCHALL_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, CATCHALL_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -57,7 +71,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@@ -66,7 +80,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "website-name" as const,
|
type: "website-name" as const,
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||||
@@ -17,11 +19,14 @@ export class CatchallGeneratorStrategy
|
|||||||
/** Instantiates the generation strategy
|
/** Instantiates the generation strategy
|
||||||
* @param usernameService generates a catchall address for a domain
|
* @param usernameService generates a catchall address for a domain
|
||||||
*/
|
*/
|
||||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private usernameService: UsernameGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return CATCHALL_SETTINGS;
|
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@@ -38,6 +43,10 @@ export class CatchallGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy) {
|
evaluator(policy: Policy) {
|
||||||
|
if (!policy) {
|
||||||
|
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("EFF long word list generation strategy", () => {
|
describe("EFF long word list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new EffUsernameGeneratorStrategy(null);
|
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@@ -21,7 +25,7 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new EffUsernameGeneratorStrategy(null);
|
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@@ -34,21 +38,31 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({});
|
expect(evaluator.policy).toMatchObject({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map `null` to a default policy evaluator", () => {
|
||||||
|
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||||
|
const evaluator = strategy.evaluator(null);
|
||||||
|
|
||||||
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(EFF_USERNAME_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, EFF_USERNAME_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -57,7 +71,7 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@@ -66,7 +80,7 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
wordCapitalize: false,
|
wordCapitalize: false,
|
||||||
wordIncludeNumber: false,
|
wordIncludeNumber: false,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
@@ -17,11 +19,14 @@ export class EffUsernameGeneratorStrategy
|
|||||||
/** Instantiates the generation strategy
|
/** Instantiates the generation strategy
|
||||||
* @param usernameService generates a username from EFF word list
|
* @param usernameService generates a username from EFF word list
|
||||||
*/
|
*/
|
||||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private usernameService: UsernameGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return EFF_USERNAME_SETTINGS;
|
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@@ -38,6 +43,10 @@ export class EffUsernameGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy) {
|
evaluator(policy: Policy) {
|
||||||
|
if (!policy) {
|
||||||
|
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import { PolicyType } from "../../../admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Email subaddress list generation strategy", () => {
|
describe("Email subaddress list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new SubaddressGeneratorStrategy(null);
|
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@@ -21,7 +25,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new SubaddressGeneratorStrategy(null);
|
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@@ -34,21 +38,31 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({});
|
expect(evaluator.policy).toMatchObject({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map `null` to a default policy evaluator", () => {
|
||||||
|
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||||
|
const evaluator = strategy.evaluator(null);
|
||||||
|
|
||||||
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(SUBADDRESS_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, SUBADDRESS_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -57,7 +71,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@@ -66,7 +80,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "website-name" as const,
|
type: "website-name" as const,
|
||||||
email: "someone@example.com",
|
email: "someone@example.com",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
@@ -17,11 +19,14 @@ export class SubaddressGeneratorStrategy
|
|||||||
/** Instantiates the generation strategy
|
/** Instantiates the generation strategy
|
||||||
* @param usernameService generates an email subaddress from an email address
|
* @param usernameService generates an email subaddress from an email address
|
||||||
*/
|
*/
|
||||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private usernameService: UsernameGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return SUBADDRESS_SETTINGS;
|
return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@@ -38,6 +43,10 @@ export class SubaddressGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy) {
|
evaluator(policy: Policy) {
|
||||||
|
if (!policy) {
|
||||||
|
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
|||||||
161
package-lock.json
generated
161
package-lock.json
generated
@@ -16,7 +16,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "16.2.12",
|
"@angular/animations": "16.2.12",
|
||||||
"@angular/cdk": "16.2.13",
|
"@angular/cdk": "16.2.14",
|
||||||
"@angular/common": "16.2.12",
|
"@angular/common": "16.2.12",
|
||||||
"@angular/compiler": "16.2.12",
|
"@angular/compiler": "16.2.12",
|
||||||
"@angular/core": "16.2.12",
|
"@angular/core": "16.2.12",
|
||||||
@@ -159,8 +159,8 @@
|
|||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"node-ipc": "9.2.1",
|
"node-ipc": "9.2.1",
|
||||||
"pkg": "5.8.1",
|
"pkg": "5.8.1",
|
||||||
"postcss": "8.4.32",
|
"postcss": "8.4.35",
|
||||||
"postcss-loader": "7.3.3",
|
"postcss-loader": "7.3.4",
|
||||||
"prettier": "3.2.2",
|
"prettier": "3.2.2",
|
||||||
"prettier-plugin-tailwindcss": "0.5.11",
|
"prettier-plugin-tailwindcss": "0.5.11",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"rimraf": "5.0.5",
|
"rimraf": "5.0.5",
|
||||||
"sass": "1.69.5",
|
"sass": "1.69.5",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "13.3.3",
|
||||||
"storybook": "7.6.17",
|
"storybook": "7.6.17",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.1",
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@bitwarden/web-vault",
|
"name": "@bitwarden/web-vault",
|
||||||
"version": "2024.2.3"
|
"version": "2024.2.4"
|
||||||
},
|
},
|
||||||
"libs/admin-console": {
|
"libs/admin-console": {
|
||||||
"name": "@bitwarden/admin-console",
|
"name": "@bitwarden/admin-console",
|
||||||
@@ -576,6 +576,12 @@
|
|||||||
"ajv": "^6.9.1"
|
"ajv": "^6.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/build-angular/node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": {
|
"node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": {
|
||||||
"version": "10.4.14",
|
"version": "10.4.14",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
||||||
@@ -609,6 +615,32 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/build-angular/node_modules/cosmiconfig": {
|
||||||
|
"version": "8.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||||
|
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"import-fresh": "^3.3.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"parse-json": "^5.2.0",
|
||||||
|
"path-type": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/d-fischer"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.9.5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/eslint-scope": {
|
"node_modules/@angular-devkit/build-angular/node_modules/eslint-scope": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||||
@@ -695,6 +727,18 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/build-angular/node_modules/js-yaml": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": {
|
"node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
@@ -729,6 +773,28 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/build-angular/node_modules/postcss-loader": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cosmiconfig": "^8.2.0",
|
||||||
|
"jiti": "^1.18.2",
|
||||||
|
"semver": "^7.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.15.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"postcss": "^7.0.0 || ^8.0.1",
|
||||||
|
"webpack": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/sass": {
|
"node_modules/@angular-devkit/build-angular/node_modules/sass": {
|
||||||
"version": "1.64.1",
|
"version": "1.64.1",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz",
|
||||||
@@ -746,6 +812,43 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular-devkit/build-angular/node_modules/sass-loader": {
|
||||||
|
"version": "13.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz",
|
||||||
|
"integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"neo-async": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.15.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"fibers": ">= 3.1.0",
|
||||||
|
"node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||||
|
"sass": "^1.3.0",
|
||||||
|
"sass-embedded": "*",
|
||||||
|
"webpack": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"fibers": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node-sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular-devkit/build-angular/node_modules/schema-utils": {
|
"node_modules/@angular-devkit/build-angular/node_modules/schema-utils": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||||
@@ -965,9 +1068,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular/cdk": {
|
"node_modules/@angular/cdk": {
|
||||||
"version": "16.2.13",
|
"version": "16.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.14.tgz",
|
||||||
"integrity": "sha512-8kn2X2yesvgfIbCUNoS9EDjooIx9LwEglYBbD89Y/do8EeN/CC3Tn02gqSrEfgMhYBLBJmHXbfOhbDDvcvOCeg==",
|
"integrity": "sha512-n6PrGdiVeSTEmM/HEiwIyg6YQUUymZrb5afaNLGFRM5YL0Y8OBqd+XhCjb0OfD/AfgCUtedVEPwNqrfW8KzgGw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@@ -30701,9 +30804,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.32",
|
"version": "8.4.35",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
|
||||||
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
|
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -30803,14 +30906,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-loader": {
|
"node_modules/postcss-loader": {
|
||||||
"version": "7.3.3",
|
"version": "7.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz",
|
||||||
"integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==",
|
"integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cosmiconfig": "^8.2.0",
|
"cosmiconfig": "^8.3.5",
|
||||||
"jiti": "^1.18.2",
|
"jiti": "^1.20.0",
|
||||||
"semver": "^7.3.8"
|
"semver": "^7.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.15.0"
|
"node": ">= 14.15.0"
|
||||||
@@ -30831,14 +30934,14 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/postcss-loader/node_modules/cosmiconfig": {
|
"node_modules/postcss-loader/node_modules/cosmiconfig": {
|
||||||
"version": "8.2.0",
|
"version": "8.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||||
"integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==",
|
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"import-fresh": "^3.2.1",
|
"import-fresh": "^3.3.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"parse-json": "^5.0.0",
|
"parse-json": "^5.2.0",
|
||||||
"path-type": "^4.0.0"
|
"path-type": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -30846,6 +30949,14 @@
|
|||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/d-fischer"
|
"url": "https://github.com/sponsors/d-fischer"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.9.5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-loader/node_modules/js-yaml": {
|
"node_modules/postcss-loader/node_modules/js-yaml": {
|
||||||
@@ -33122,9 +33233,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass-loader": {
|
"node_modules/sass-loader": {
|
||||||
"version": "13.3.2",
|
"version": "13.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz",
|
||||||
"integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==",
|
"integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"neo-async": "^2.6.2"
|
"neo-async": "^2.6.2"
|
||||||
|
|||||||
@@ -120,8 +120,8 @@
|
|||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"node-ipc": "9.2.1",
|
"node-ipc": "9.2.1",
|
||||||
"pkg": "5.8.1",
|
"pkg": "5.8.1",
|
||||||
"postcss": "8.4.32",
|
"postcss": "8.4.35",
|
||||||
"postcss-loader": "7.3.3",
|
"postcss-loader": "7.3.4",
|
||||||
"prettier": "3.2.2",
|
"prettier": "3.2.2",
|
||||||
"prettier-plugin-tailwindcss": "0.5.11",
|
"prettier-plugin-tailwindcss": "0.5.11",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"rimraf": "5.0.5",
|
"rimraf": "5.0.5",
|
||||||
"sass": "1.69.5",
|
"sass": "1.69.5",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "13.3.3",
|
||||||
"storybook": "7.6.17",
|
"storybook": "7.6.17",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.1",
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "16.2.12",
|
"@angular/animations": "16.2.12",
|
||||||
"@angular/cdk": "16.2.13",
|
"@angular/cdk": "16.2.14",
|
||||||
"@angular/common": "16.2.12",
|
"@angular/common": "16.2.12",
|
||||||
"@angular/compiler": "16.2.12",
|
"@angular/compiler": "16.2.12",
|
||||||
"@angular/core": "16.2.12",
|
"@angular/core": "16.2.12",
|
||||||
|
|||||||
Reference in New Issue
Block a user