From 4ae208fabc9b0d3e4507ca3cfee78bdd8933a7cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Fri, 10 May 2024 16:08:05 -0400 Subject: [PATCH 01/33] rotate sends from original key to rotated key (#9130) --- .../src/tools/send/services/send.service.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index fb67de5501..aad0b887c8 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -263,18 +263,26 @@ export class SendService implements InternalSendServiceAbstraction { throw new Error("New user key is required for rotation."); } + const originalUserKey = await this.cryptoService.getUserKey(); + const req = await firstValueFrom( - this.sends$.pipe(concatMap(async (sends) => this.toRotatedKeyRequestMap(sends, newUserKey))), + this.sends$.pipe( + concatMap(async (sends) => this.toRotatedKeyRequestMap(sends, originalUserKey, newUserKey)), + ), ); // separate return for easier debugging return req; } - private async toRotatedKeyRequestMap(sends: Send[], newUserKey: UserKey) { + private async toRotatedKeyRequestMap( + sends: Send[], + originalUserKey: UserKey, + rotateUserKey: UserKey, + ) { const requests = await Promise.all( sends.map(async (send) => { - const sendKey = await this.encryptService.decryptToBytes(send.key, newUserKey); - send.key = await this.encryptService.encrypt(sendKey, newUserKey); + const sendKey = await this.encryptService.decryptToBytes(send.key, originalUserKey); + send.key = await this.encryptService.encrypt(sendKey, rotateUserKey); return new SendWithIdRequest(send); }), ); From a141d06c00f030adf31a372b197cf37365edf098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Sat, 11 May 2024 14:54:12 +0100 Subject: [PATCH 02/33] =?UTF-8?q?[AC-2484]=C2=A0Fix=20bug=20where=20Custom?= =?UTF-8?q?=20Users=20with=20"Delete=20any=20collection"=20permission=20in?= =?UTF-8?q?correctly=20see=20"Can=20Edit"=20permission=20for=20Unassigned?= =?UTF-8?q?=20Collection=20(#8858)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [AC-2484] Fix bug where Custom Users with "Delete any collection" permission incorrectly see "Can Edit" permission for Unassigned Collection * [AC-2484] Undo change on permission tooltip permission check * [AC-2484] Fix permission text for unassigned collection --- .../vault-items/vault-collection-row.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 3090eeea42..75f2cf8322 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -56,14 +56,16 @@ export class VaultCollectionRowComponent { } get permissionText() { - if (this.collection.id != Unassigned && !(this.collection as CollectionAdminView).assigned) { - return this.i18nService.t("noAccess"); - } else { + if (this.collection.id == Unassigned && this.organization?.canEditUnassignedCiphers()) { + return this.i18nService.t("canEdit"); + } + if ((this.collection as CollectionAdminView).assigned) { const permissionList = getPermissionList(this.organization?.flexibleCollections); return this.i18nService.t( permissionList.find((p) => p.perm === convertToPermission(this.collection))?.labelId, ); } + return this.i18nService.t("noAccess"); } get permissionTooltip() { From c0216e191a9b41d6ff61f2ca4f0a4d9fec4f3ac1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 13 May 2024 06:51:53 -0400 Subject: [PATCH 03/33] Use encrypt service in node env secure storage (#9099) * Use `EncryptService` In `NodeEnvSecureStorage` To Replace Deprecated Methods * Update Abstract EncryptService Method To Reflect Implementation --- apps/cli/src/bw.ts | 2 +- .../platform/services/node-env-secure-storage.service.ts | 8 ++++---- libs/common/src/platform/abstractions/encrypt.service.ts | 5 +---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index d7ce250a71..2f5eefdbb2 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -272,7 +272,7 @@ export class Main { this.secureStorageService = new NodeEnvSecureStorageService( this.storageService, this.logService, - () => this.cryptoService, + this.encryptService, ); this.memoryStorageService = new MemoryStorageService(); diff --git a/apps/cli/src/platform/services/node-env-secure-storage.service.ts b/apps/cli/src/platform/services/node-env-secure-storage.service.ts index 2364553d2a..2ab18b6c46 100644 --- a/apps/cli/src/platform/services/node-env-secure-storage.service.ts +++ b/apps/cli/src/platform/services/node-env-secure-storage.service.ts @@ -1,6 +1,6 @@ import { throwError } from "rxjs"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -11,7 +11,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService { constructor( private storageService: AbstractStorageService, private logService: LogService, - private cryptoService: () => CryptoService, + private encryptService: EncryptService, ) {} get valuesRequireDeserialization(): boolean { @@ -59,7 +59,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService { if (sessionKey == null) { throw new Error("No session key available."); } - const encValue = await this.cryptoService().encryptToBytes( + const encValue = await this.encryptService.encryptToBytes( Utils.fromB64ToArray(plainValue), sessionKey, ); @@ -78,7 +78,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService { } const encBuf = EncArrayBuffer.fromB64(encValue); - const decValue = await this.cryptoService().decryptFromBytes(encBuf, sessionKey); + const decValue = await this.encryptService.decryptToBytes(encBuf, sessionKey); if (decValue == null) { this.logService.info("Failed to decrypt."); return null; diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index 9b4dde3676..bc526e3578 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -7,10 +7,7 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; - abstract encryptToBytes( - plainValue: Uint8Array, - key?: SymmetricCryptoKey, - ): Promise; + abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise; abstract decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise; abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; From 657807c96a2e30d371cbd28fc23252c00e9f00ca Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 13 May 2024 07:42:29 -0400 Subject: [PATCH 04/33] [PM-7985] Add & Use InlineDerivedStateProvider (#9131) * Add & Use InlineDerivedStateProvider * Remove Comment * Delete Foreground & Background Derived State --- .../browser/src/background/main.background.ts | 4 +- .../derived-state-provider.factory.ts | 11 +- .../background-derived-state.provider.ts | 23 ---- .../state/background-derived-state.ts | 107 ---------------- .../state/derived-state-interactions.spec.ts | 117 ------------------ .../foreground-derived-state.provider.ts | 26 ---- .../state/foreground-derived-state.spec.ts | 61 --------- .../state/foreground-derived-state.ts | 115 ----------------- .../src/popup/services/services.module.ts | 7 +- .../inline-derived-state.spec.ts | 62 ++++++++++ .../implementations/inline-derived-state.ts | 37 ++++++ 11 files changed, 108 insertions(+), 462 deletions(-) delete mode 100644 apps/browser/src/platform/state/background-derived-state.provider.ts delete mode 100644 apps/browser/src/platform/state/background-derived-state.ts delete mode 100644 apps/browser/src/platform/state/derived-state-interactions.spec.ts delete mode 100644 apps/browser/src/platform/state/foreground-derived-state.provider.ts delete mode 100644 apps/browser/src/platform/state/foreground-derived-state.spec.ts delete mode 100644 apps/browser/src/platform/state/foreground-derived-state.ts create mode 100644 libs/common/src/platform/state/implementations/inline-derived-state.spec.ts create mode 100644 libs/common/src/platform/state/implementations/inline-derived-state.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7287e9b285..6120155334 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -125,6 +125,7 @@ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state 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 { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; +import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; /* eslint-enable import/no-restricted-paths */ import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; @@ -224,7 +225,6 @@ import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; -import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; @@ -495,7 +495,7 @@ export default class MainBackground { this.accountService, this.singleUserStateProvider, ); - this.derivedStateProvider = new BackgroundDerivedStateProvider(); + this.derivedStateProvider = new InlineDerivedStateProvider(); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, this.singleUserStateProvider, diff --git a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts index 3c3900144b..2c5f8f2419 100644 --- a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts +++ b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts @@ -1,6 +1,6 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state"; - -import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider"; +// eslint-disable-next-line import/no-restricted-paths -- For dependency creation +import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; import { CachedServices, FactoryOptions, factory } from "./factory-options"; @@ -12,10 +12,5 @@ export async function derivedStateProviderFactory( cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices, opts: DerivedStateProviderInitOptions, ): Promise { - return factory( - cache, - "derivedStateProvider", - opts, - async () => new BackgroundDerivedStateProvider(), - ); + return factory(cache, "derivedStateProvider", opts, async () => new InlineDerivedStateProvider()); } diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts deleted file mode 100644 index cbc5a34b37..0000000000 --- a/apps/browser/src/platform/state/background-derived-state.provider.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Observable } from "rxjs"; - -import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; -// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client -import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; -import { DerivedStateDependencies } from "@bitwarden/common/src/types/state"; - -import { BackgroundDerivedState } from "./background-derived-state"; - -export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider { - override buildDerivedState( - parentState$: Observable, - deriveDefinition: DeriveDefinition, - dependencies: TDeps, - ): DerivedState { - return new BackgroundDerivedState( - parentState$, - deriveDefinition, - deriveDefinition.buildCacheKey(), - dependencies, - ); - } -} diff --git a/apps/browser/src/platform/state/background-derived-state.ts b/apps/browser/src/platform/state/background-derived-state.ts deleted file mode 100644 index 61768cb970..0000000000 --- a/apps/browser/src/platform/state/background-derived-state.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Observable, Subscription, concatMap } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DeriveDefinition } from "@bitwarden/common/platform/state"; -// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client -import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state"; -import { DerivedStateDependencies } from "@bitwarden/common/types/state"; - -import { BrowserApi } from "../browser/browser-api"; - -export class BackgroundDerivedState< - TFrom, - TTo, - TDeps extends DerivedStateDependencies, -> extends DefaultDerivedState { - private portSubscriptions: Map = new Map(); - - constructor( - parentState$: Observable, - deriveDefinition: DeriveDefinition, - portName: string, - dependencies: TDeps, - ) { - super(parentState$, deriveDefinition, dependencies); - - // listen for foreground derived states to connect - BrowserApi.addListener(chrome.runtime.onConnect, (port) => { - if (port.name !== portName) { - return; - } - - const listenerCallback = this.onMessageFromForeground.bind(this); - port.onDisconnect.addListener(() => { - this.portSubscriptions.get(port)?.unsubscribe(); - this.portSubscriptions.delete(port); - port.onMessage.removeListener(listenerCallback); - }); - port.onMessage.addListener(listenerCallback); - - const stateSubscription = this.state$ - .pipe( - concatMap(async (state) => { - await this.sendMessage( - { - action: "nextState", - data: JSON.stringify(state), - id: Utils.newGuid(), - }, - port, - ); - }), - ) - .subscribe(); - - this.portSubscriptions.set(port, stateSubscription); - }); - } - - private async onMessageFromForeground(message: DerivedStateMessage, port: chrome.runtime.Port) { - if (message.originator === "background") { - return; - } - - switch (message.action) { - case "nextState": { - const dataObj = JSON.parse(message.data) as Jsonify; - const data = this.deriveDefinition.deserialize(dataObj); - await this.forceValue(data); - await this.sendResponse( - message, - { - action: "resolve", - }, - port, - ); - break; - } - } - } - - private async sendResponse( - originalMessage: DerivedStateMessage, - response: Omit, - port: chrome.runtime.Port, - ) { - // 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 - this.sendMessage( - { - ...response, - id: originalMessage.id, - }, - port, - ); - } - - private async sendMessage( - message: Omit, - port: chrome.runtime.Port, - ) { - port.postMessage({ - ...message, - originator: "background", - }); - } -} diff --git a/apps/browser/src/platform/state/derived-state-interactions.spec.ts b/apps/browser/src/platform/state/derived-state-interactions.spec.ts deleted file mode 100644 index 823c071a4c..0000000000 --- a/apps/browser/src/platform/state/derived-state-interactions.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * need to update test environment so structuredClone works appropriately - * @jest-environment ../../libs/shared/test.environment.ts - */ - -import { NgZone } from "@angular/core"; -import { mock } from "jest-mock-extended"; -import { Subject, firstValueFrom } from "rxjs"; - -import { DeriveDefinition } from "@bitwarden/common/platform/state"; -// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition -import { StateDefinition } from "@bitwarden/common/platform/state/state-definition"; -import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec"; - -import { mockPorts } from "../../../spec/mock-port.spec-util"; - -import { BackgroundDerivedState } from "./background-derived-state"; -import { ForegroundDerivedState } from "./foreground-derived-state"; - -const stateDefinition = new StateDefinition("test", "memory"); -const deriveDefinition = new DeriveDefinition(stateDefinition, "test", { - derive: (dateString: string) => (dateString == null ? null : new Date(dateString)), - deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)), - cleanupDelayMs: 1000, -}); - -// Mock out the runInsideAngular operator so we don't have to deal with zone.js -jest.mock("../browser/run-inside-angular.operator", () => { - return { - runInsideAngular: (ngZone: any) => (source: any) => source, - }; -}); - -describe("foreground background derived state interactions", () => { - let foreground: ForegroundDerivedState; - let background: BackgroundDerivedState>; - let parentState$: Subject; - const initialParent = "2020-01-01"; - const ngZone = mock(); - const portName = "testPort"; - - beforeEach(() => { - mockPorts(); - parentState$ = new Subject(); - - background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {}); - foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); - }); - - afterEach(() => { - parentState$.complete(); - jest.resetAllMocks(); - }); - - it("should connect between foreground and background", async () => { - const foregroundEmissions = trackEmissions(foreground.state$); - const backgroundEmissions = trackEmissions(background.state$); - - parentState$.next(initialParent); - await awaitAsync(10); - - expect(backgroundEmissions).toEqual([new Date(initialParent)]); - expect(foregroundEmissions).toEqual([new Date(initialParent)]); - }); - - it("should initialize a late-connected foreground", async () => { - const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); - const backgroundTracker = new ObservableTracker(background.state$); - parentState$.next(initialParent); - const foregroundTracker = new ObservableTracker(newForeground.state$); - - expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent)); - expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent)); - }); - - describe("forceValue", () => { - it("should force the value to the background", async () => { - const dateString = "2020-12-12"; - const emissions = trackEmissions(background.state$); - - await foreground.forceValue(new Date(dateString)); - await awaitAsync(); - - expect(emissions).toEqual([new Date(dateString)]); - }); - - it("should not create new ports if already connected", async () => { - // establish port with subscription - trackEmissions(foreground.state$); - - const connectMock = chrome.runtime.connect as jest.Mock; - const initialConnectCalls = connectMock.mock.calls.length; - - expect(foreground["port"]).toBeDefined(); - const newDate = new Date(); - await foreground.forceValue(newDate); - await awaitAsync(); - - expect(connectMock.mock.calls.length).toBe(initialConnectCalls); - expect(await firstValueFrom(background.state$)).toEqual(newDate); - }); - - it("should create a port if not connected", async () => { - const connectMock = chrome.runtime.connect as jest.Mock; - const initialConnectCalls = connectMock.mock.calls.length; - - expect(foreground["port"]).toBeUndefined(); - const newDate = new Date(); - await foreground.forceValue(newDate); - await awaitAsync(); - - expect(connectMock.mock.calls.length).toBe(initialConnectCalls + 1); - expect(foreground["port"]).toBeNull(); - expect(await firstValueFrom(background.state$)).toEqual(newDate); - }); - }); -}); diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts deleted file mode 100644 index 8b8d82b914..0000000000 --- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgZone } from "@angular/core"; -import { Observable } from "rxjs"; - -import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; -// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client -import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; -import { DerivedStateDependencies } from "@bitwarden/common/src/types/state"; - -import { ForegroundDerivedState } from "./foreground-derived-state"; - -export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider { - constructor(private ngZone: NgZone) { - super(); - } - override buildDerivedState( - _parentState$: Observable, - deriveDefinition: DeriveDefinition, - _dependencies: TDeps, - ): DerivedState { - return new ForegroundDerivedState( - deriveDefinition, - deriveDefinition.buildCacheKey(), - this.ngZone, - ); - } -} diff --git a/apps/browser/src/platform/state/foreground-derived-state.spec.ts b/apps/browser/src/platform/state/foreground-derived-state.spec.ts deleted file mode 100644 index ee224540c1..0000000000 --- a/apps/browser/src/platform/state/foreground-derived-state.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NgZone } from "@angular/core"; -import { awaitAsync } from "@bitwarden/common/../spec"; -import { mock } from "jest-mock-extended"; - -import { DeriveDefinition } from "@bitwarden/common/platform/state"; -// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition -import { StateDefinition } from "@bitwarden/common/platform/state/state-definition"; - -import { mockPorts } from "../../../spec/mock-port.spec-util"; - -import { ForegroundDerivedState } from "./foreground-derived-state"; - -const stateDefinition = new StateDefinition("test", "memory"); -const deriveDefinition = new DeriveDefinition(stateDefinition, "test", { - derive: (dateString: string) => (dateString == null ? null : new Date(dateString)), - deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)), - cleanupDelayMs: 1, -}); - -// Mock out the runInsideAngular operator so we don't have to deal with zone.js -jest.mock("../browser/run-inside-angular.operator", () => { - return { - runInsideAngular: (ngZone: any) => (source: any) => source, - }; -}); - -describe("ForegroundDerivedState", () => { - let sut: ForegroundDerivedState; - const portName = "testPort"; - const ngZone = mock(); - - beforeEach(() => { - mockPorts(); - sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should not connect a port until subscribed", async () => { - expect(sut["port"]).toBeUndefined(); - const subscription = sut.state$.subscribe(); - - expect(sut["port"]).toBeDefined(); - subscription.unsubscribe(); - }); - - it("should disconnect its port when unsubscribed", async () => { - const subscription = sut.state$.subscribe(); - - expect(sut["port"]).toBeDefined(); - const disconnectSpy = jest.spyOn(sut["port"], "disconnect"); - subscription.unsubscribe(); - // wait for the cleanup delay - await awaitAsync(deriveDefinition.cleanupDelayMs * 2); - - expect(disconnectSpy).toHaveBeenCalled(); - expect(sut["port"]).toBeNull(); - }); -}); diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts deleted file mode 100644 index 6abe363876..0000000000 --- a/apps/browser/src/platform/state/foreground-derived-state.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { NgZone } from "@angular/core"; -import { - Observable, - ReplaySubject, - defer, - filter, - firstValueFrom, - map, - of, - share, - switchMap, - tap, - timer, -} from "rxjs"; -import { Jsonify } from "type-fest"; - -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; -import { DerivedStateDependencies } from "@bitwarden/common/types/state"; - -import { fromChromeEvent } from "../browser/from-chrome-event"; -import { runInsideAngular } from "../browser/run-inside-angular.operator"; - -export class ForegroundDerivedState implements DerivedState { - private port: chrome.runtime.Port; - private backgroundResponses$: Observable; - state$: Observable; - - constructor( - private deriveDefinition: DeriveDefinition, - private portName: string, - private ngZone: NgZone, - ) { - const latestValueFromPort$ = (port: chrome.runtime.Port) => { - return fromChromeEvent(port.onMessage).pipe( - map(([message]) => message as DerivedStateMessage), - filter((message) => message.originator === "background" && message.action === "nextState"), - map((message) => { - const json = JSON.parse(message.data) as Jsonify; - return this.deriveDefinition.deserialize(json); - }), - ); - }; - - this.state$ = defer(() => of(this.initializePort())).pipe( - switchMap(() => latestValueFromPort$(this.port)), - share({ - connector: () => new ReplaySubject(1), - resetOnRefCountZero: () => - timer(this.deriveDefinition.cleanupDelayMs).pipe(tap(() => this.tearDownPort())), - }), - runInsideAngular(this.ngZone), - ); - } - - async forceValue(value: TTo): Promise { - let cleanPort = false; - if (this.port == null) { - this.initializePort(); - cleanPort = true; - } - await this.delegateToBackground("nextState", value); - if (cleanPort) { - this.tearDownPort(); - } - return value; - } - - private initializePort() { - if (this.port != null) { - return; - } - - this.port = chrome.runtime.connect({ name: this.portName }); - - this.backgroundResponses$ = fromChromeEvent(this.port.onMessage).pipe( - map(([message]) => message as DerivedStateMessage), - filter((message) => message.originator === "background"), - ); - return this.backgroundResponses$; - } - - private async delegateToBackground(action: DerivedStateActions, data: TTo): Promise { - const id = Utils.newGuid(); - // listen for response before request - const response = firstValueFrom( - this.backgroundResponses$.pipe(filter((message) => message.id === id)), - ); - - this.sendMessage({ - id, - action, - data: JSON.stringify(data), - }); - - await response; - } - - private sendMessage(message: Omit) { - this.port.postMessage({ - ...message, - originator: "foreground", - }); - } - - private tearDownPort() { - if (this.port == null) { - return; - } - - this.port.disconnect(); - this.port = null; - this.backgroundResponses$ = null; - } -} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index de2fc72747..2313833ab8 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -78,6 +78,8 @@ import { GlobalStateProvider, StateProvider, } from "@bitwarden/common/platform/state"; +// eslint-disable-next-line import/no-restricted-paths -- Used for dependency injection +import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -112,7 +114,6 @@ import { BrowserScriptInjectorService } from "../../platform/services/browser-sc import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; -import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; @@ -512,8 +513,8 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: DerivedStateProvider, - useClass: ForegroundDerivedStateProvider, - deps: [NgZone], + useClass: InlineDerivedStateProvider, + deps: [], }), safeProvider({ provide: AutofillSettingsServiceAbstraction, diff --git a/libs/common/src/platform/state/implementations/inline-derived-state.spec.ts b/libs/common/src/platform/state/implementations/inline-derived-state.spec.ts new file mode 100644 index 0000000000..28a1a701b0 --- /dev/null +++ b/libs/common/src/platform/state/implementations/inline-derived-state.spec.ts @@ -0,0 +1,62 @@ +import { Subject, firstValueFrom } from "rxjs"; + +import { DeriveDefinition } from "../derive-definition"; +import { StateDefinition } from "../state-definition"; + +import { InlineDerivedState } from "./inline-derived-state"; + +describe("InlineDerivedState", () => { + const syncDeriveDefinition = new DeriveDefinition>( + new StateDefinition("test", "disk"), + "test", + { + derive: (value, deps) => !value, + deserializer: (value) => value, + }, + ); + + const asyncDeriveDefinition = new DeriveDefinition>( + new StateDefinition("test", "disk"), + "test", + { + derive: async (value, deps) => Promise.resolve(!value), + deserializer: (value) => value, + }, + ); + + const parentState = new Subject(); + + describe("state", () => { + const cases = [ + { + it: "works when derive function is sync", + definition: syncDeriveDefinition, + }, + { + it: "works when derive function is async", + definition: asyncDeriveDefinition, + }, + ]; + + it.each(cases)("$it", async ({ definition }) => { + const sut = new InlineDerivedState(parentState.asObservable(), definition, {}); + + const valuePromise = firstValueFrom(sut.state$); + parentState.next(true); + + const value = await valuePromise; + + expect(value).toBe(false); + }); + }); + + describe("forceValue", () => { + it("returns the force value back to the caller", async () => { + const sut = new InlineDerivedState(parentState.asObservable(), syncDeriveDefinition, {}); + + const value = await sut.forceValue(true); + + expect(value).toBe(true); + }); + }); +}); diff --git a/libs/common/src/platform/state/implementations/inline-derived-state.ts b/libs/common/src/platform/state/implementations/inline-derived-state.ts new file mode 100644 index 0000000000..79b2c92100 --- /dev/null +++ b/libs/common/src/platform/state/implementations/inline-derived-state.ts @@ -0,0 +1,37 @@ +import { Observable, concatMap } from "rxjs"; + +import { DerivedStateDependencies } from "../../../types/state"; +import { DeriveDefinition } from "../derive-definition"; +import { DerivedState } from "../derived-state"; +import { DerivedStateProvider } from "../derived-state.provider"; + +export class InlineDerivedStateProvider implements DerivedStateProvider { + get( + parentState$: Observable, + deriveDefinition: DeriveDefinition, + dependencies: TDeps, + ): DerivedState { + return new InlineDerivedState(parentState$, deriveDefinition, dependencies); + } +} + +export class InlineDerivedState + implements DerivedState +{ + constructor( + parentState$: Observable, + deriveDefinition: DeriveDefinition, + dependencies: TDeps, + ) { + this.state$ = parentState$.pipe( + concatMap(async (value) => await deriveDefinition.derive(value, dependencies)), + ); + } + + state$: Observable; + + forceValue(value: TTo): Promise { + // No need to force anything, we don't keep a cache + return Promise.resolve(value); + } +} From 611304a921bf07d42eb82aa35a9297ef30eb930a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 12:31:08 +0000 Subject: [PATCH 05/33] Autosync the updated translations (#9112) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 6 ++++++ apps/web/src/locales/ar/messages.json | 6 ++++++ apps/web/src/locales/az/messages.json | 8 +++++++- apps/web/src/locales/be/messages.json | 6 ++++++ apps/web/src/locales/bg/messages.json | 6 ++++++ apps/web/src/locales/bn/messages.json | 6 ++++++ apps/web/src/locales/bs/messages.json | 6 ++++++ apps/web/src/locales/ca/messages.json | 8 +++++++- apps/web/src/locales/cs/messages.json | 6 ++++++ apps/web/src/locales/cy/messages.json | 6 ++++++ apps/web/src/locales/da/messages.json | 6 ++++++ apps/web/src/locales/de/messages.json | 8 +++++++- apps/web/src/locales/el/messages.json | 6 ++++++ apps/web/src/locales/en_GB/messages.json | 6 ++++++ apps/web/src/locales/en_IN/messages.json | 6 ++++++ apps/web/src/locales/eo/messages.json | 6 ++++++ apps/web/src/locales/es/messages.json | 6 ++++++ apps/web/src/locales/et/messages.json | 6 ++++++ apps/web/src/locales/eu/messages.json | 6 ++++++ apps/web/src/locales/fa/messages.json | 6 ++++++ apps/web/src/locales/fi/messages.json | 6 ++++++ apps/web/src/locales/fil/messages.json | 6 ++++++ apps/web/src/locales/fr/messages.json | 6 ++++++ apps/web/src/locales/gl/messages.json | 6 ++++++ apps/web/src/locales/he/messages.json | 6 ++++++ apps/web/src/locales/hi/messages.json | 6 ++++++ apps/web/src/locales/hr/messages.json | 6 ++++++ apps/web/src/locales/hu/messages.json | 6 ++++++ apps/web/src/locales/id/messages.json | 6 ++++++ apps/web/src/locales/it/messages.json | 6 ++++++ apps/web/src/locales/ja/messages.json | 6 ++++++ apps/web/src/locales/ka/messages.json | 6 ++++++ apps/web/src/locales/km/messages.json | 6 ++++++ apps/web/src/locales/kn/messages.json | 6 ++++++ apps/web/src/locales/ko/messages.json | 6 ++++++ apps/web/src/locales/lv/messages.json | 12 +++++++++--- apps/web/src/locales/ml/messages.json | 6 ++++++ apps/web/src/locales/mr/messages.json | 6 ++++++ apps/web/src/locales/my/messages.json | 6 ++++++ apps/web/src/locales/nb/messages.json | 6 ++++++ apps/web/src/locales/ne/messages.json | 6 ++++++ apps/web/src/locales/nl/messages.json | 6 ++++++ apps/web/src/locales/nn/messages.json | 6 ++++++ apps/web/src/locales/or/messages.json | 6 ++++++ apps/web/src/locales/pl/messages.json | 6 ++++++ apps/web/src/locales/pt_BR/messages.json | 6 ++++++ apps/web/src/locales/pt_PT/messages.json | 6 ++++++ apps/web/src/locales/ro/messages.json | 6 ++++++ apps/web/src/locales/ru/messages.json | 6 ++++++ apps/web/src/locales/si/messages.json | 6 ++++++ apps/web/src/locales/sk/messages.json | 8 +++++++- apps/web/src/locales/sl/messages.json | 6 ++++++ apps/web/src/locales/sr/messages.json | 22 ++++++++++++++-------- apps/web/src/locales/sr_CS/messages.json | 6 ++++++ apps/web/src/locales/sv/messages.json | 6 ++++++ apps/web/src/locales/te/messages.json | 6 ++++++ apps/web/src/locales/th/messages.json | 6 ++++++ apps/web/src/locales/tr/messages.json | 6 ++++++ apps/web/src/locales/uk/messages.json | 6 ++++++ apps/web/src/locales/vi/messages.json | 6 ++++++ apps/web/src/locales/zh_CN/messages.json | 6 ++++++ apps/web/src/locales/zh_TW/messages.json | 6 ++++++ 62 files changed, 387 insertions(+), 15 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index fd873a41f1..e26099610c 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Alle" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Verfris" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index bb99982684..018acea8f2 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "إنعاش" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 795b6bee8e..457e0c6e1f 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Hamısı" }, + "addAccess": { + "message": "Müraciət əlavə et" + }, + "addAccessFilter": { + "message": "Müraciət filtri əlavə et" + }, "refresh": { "message": "Təzələ" }, @@ -7962,7 +7968,7 @@ } }, "deleteProviderWarningDescription": { - "message": "You must unlink all clients before you can delete $ID$.", + "message": "$ID$ silinməzdən əvvəl bütün müştəriləri (client) ayırmalısınız.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index ba424f456b..24c92511ea 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Усе" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Абнавiць" }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 5b071f6658..b9c8890219 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Всички" }, + "addAccess": { + "message": "Добавяне на достъп" + }, + "addAccessFilter": { + "message": "Добавяне на филтър за достъп" + }, "refresh": { "message": "Опресняване" }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 0de85f1b9f..ff1d1d7af5 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index de846aa801..7c78284cec 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 443348f1e0..0542b36e29 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Tot" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Actualitza" }, @@ -7962,7 +7968,7 @@ } }, "deleteProviderWarningDescription": { - "message": "You must unlink all clients before you can delete $ID$.", + "message": "Heu de desenllaçar tots els clients abans de poder suprimir $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index ae9df7bb1d..cdaa76774e 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Vše" }, + "addAccess": { + "message": "Přidat přístup" + }, + "addAccessFilter": { + "message": "Přidat přístupový filtr" + }, "refresh": { "message": "Obnovit" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 791f940f11..3e206c23b1 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index f4b6559cfc..0e9ff6921e 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Alle" }, + "addAccess": { + "message": "Tilføj adgang" + }, + "addAccessFilter": { + "message": "Tilføj Adgangsfilter" + }, "refresh": { "message": "Opdater" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index f7d0e08273..357abc5de2 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Alle" }, + "addAccess": { + "message": "Zugriff hinzufügen" + }, + "addAccessFilter": { + "message": "Zugriffsfilter hinzufügen" + }, "refresh": { "message": "Aktualisieren" }, @@ -7962,7 +7968,7 @@ } }, "deleteProviderWarningDescription": { - "message": "You must unlink all clients before you can delete $ID$.", + "message": "Du musst die Verknüpfung zu allen Clients aufheben, bevor du $ID$ löschen kannst.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 6dab7f4816..332ec7c72a 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Όλα" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Ανανέωση" }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index bf746dd0d5..5fc7b328ba 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 3343910c8a..be655448a8 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 4f9ffd2f0f..cc599c2965 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Ĉiuj" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refreŝigi" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 338d172bb7..5edaab44ca 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Todo" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Actualizar" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index d2ea371f43..9bdb81baef 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Kõik" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Värskenda" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 9b208f945a..b1a04c8c41 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Guztiak" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Freskatu" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 45a32b2011..a31f0e7ddc 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "همه" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "تازه کردن" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index eb0e93114b..39fba33e80 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Kaikki" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Päivitä" }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index e3806b6541..d638415f41 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Lahat" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "I-refresh" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index eac5e78d87..5beb47d465 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Tous" }, + "addAccess": { + "message": "Ajouter Access" + }, + "addAccessFilter": { + "message": "Ajouter un filtre Access" + }, "refresh": { "message": "Actualiser" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 417c7adc6a..8c37b919af 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index c448a25e59..9b8b8bb461 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "הכל" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "רענן" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 5e0d3b8c29..0bf2d56635 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 6eac81da08..448149e7b8 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Sve" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Osvježi" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 1c99680f62..ca79bcd521 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Összes" }, + "addAccess": { + "message": "Hozzáférés hozzáadása" + }, + "addAccessFilter": { + "message": "Hozzáférés szűrő hozzáadása" + }, "refresh": { "message": "Frissítés" }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 0aacc90be7..77fa91ebf1 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Semua" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Segarkan" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 5c3894d443..ebea88daa3 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Tutti" }, + "addAccess": { + "message": "Aggiungi accesso" + }, + "addAccessFilter": { + "message": "Aggiungi filtro di accesso" + }, "refresh": { "message": "Aggiorna" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 8bf56e6f65..c43c0d1650 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "すべて" }, + "addAccess": { + "message": "アクセスを追加" + }, + "addAccessFilter": { + "message": "アクセスフィルタを追加" + }, "refresh": { "message": "更新" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 104810e936..897e1a548a 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 417c7adc6a..8c37b919af 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 3b34cc7976..11447428be 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "ಎಲ್ಲಾ" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "ರಿಫ್ರೆಶ್" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 76b86a7433..e9fc334330 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "모두" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "새로 고침" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index cd5991ce7c..7d02dbce26 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -1894,7 +1894,7 @@ "message": "Vājo paroļu pārskats" }, "weakPasswordsReportDesc": { - "message": "Vājas paroles var viegli uzminēt urķi un automatizētie rīki, kas tiek izmantoti paroļu uzlaušanā. Bitwarden paroļu veidotājs var palīdzēt izveidot spēcīgas paroles." + "message": "Uzbrucēji var viegli uzminēt vājas paroles. Šīs paroles būtu jānomaina pret spēcīgām ar paroļu veidotāja palīdzību." }, "weakPasswordsFound": { "message": "Atrastas vājas paroles" @@ -1941,7 +1941,7 @@ "message": "Glabātavā nav pieteikšanās vienumu ar vairākkārt izmantotām parolēm." }, "reusedXTimes": { - "message": "Vairākkārt izmantota(s) $COUNT$ reizi(es)", + "message": "Izmantota $COUNT$ reizes", "placeholders": { "count": { "content": "$1", @@ -2788,6 +2788,12 @@ "all": { "message": "Visi" }, + "addAccess": { + "message": "Pievienot piekļuvi" + }, + "addAccessFilter": { + "message": "Pievienot piekļuves atlasi" + }, "refresh": { "message": "Atsvaidzināt" }, @@ -7962,7 +7968,7 @@ } }, "deleteProviderWarningDescription": { - "message": "You must unlink all clients before you can delete $ID$.", + "message": "Ir jāatsaista visi klienti, pirms var izdzēst $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 3bd5ca3049..9e67aebbcb 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "എല്ലാം" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "റിഫ്രഷ് ചെയ്യുക" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 417c7adc6a..8c37b919af 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 417c7adc6a..8c37b919af 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 5b544ff3a4..e670117cb4 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Alle" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Oppfrisk" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 24aaf3d0d3..03ba792a6e 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 5a203b5a51..6ef7fbae59 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Alle" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Verversen" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 7569f5a4ee..08631216c8 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 417c7adc6a..8c37b919af 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index e1a48f0fb0..7b48a6a20c 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Wszyscy" }, + "addAccess": { + "message": "Dodaj dostęp" + }, + "addAccessFilter": { + "message": "Dodaj filtr dostępu" + }, "refresh": { "message": "Odśwież" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index fc3c4b1a51..29d7ebe4cc 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Todos" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Atualizar" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 5446a9dd65..936a8d4c9b 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Todos" }, + "addAccess": { + "message": "Adicionar acesso" + }, + "addAccessFilter": { + "message": "Adicionar filtro de acesso" + }, "refresh": { "message": "Atualizar" }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index eb40a84da9..0a9f08e39d 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Tot" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Reîmprospătare" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 92eced8799..36cd7465cc 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Все" }, + "addAccess": { + "message": "Добавить доступ" + }, + "addAccessFilter": { + "message": "Добавить фильтр доступа" + }, "refresh": { "message": "Обновить" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index f264457250..ece41d234c 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 7dd89c23be..7ed22b5a33 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Všetky" }, + "addAccess": { + "message": "Udeliť prístup" + }, + "addAccessFilter": { + "message": "Filter udelenia prístupu" + }, "refresh": { "message": "Obnoviť" }, @@ -7962,7 +7968,7 @@ } }, "deleteProviderWarningDescription": { - "message": "You must unlink all clients before you can delete $ID$.", + "message": "Pred tým než budete môcť odstrániť $ID$, musíte odpojiť všetkých klientov.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 5a85e934f0..2b6fd699c2 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Vsi" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 38bc66f911..e2ee1ad4d5 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -1810,7 +1810,7 @@ "message": "Пронађене су незаштићене веб странице" }, "unsecuredWebsitesFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "message": "Нашли смо $COUNT$ ставке у вашем $VAULT$ са незаштићеним УРЛ. Требали би да промените шеме у https:// ако веб страница то дозвољава.", "placeholders": { "count": { "content": "$1", @@ -1835,7 +1835,7 @@ "message": "Нађене пријаве без 2FA" }, "inactive2faFoundReportDesc": { - "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "message": "Нашли смо $COUNT$ сајта у вашем $VAULT$ који можда нису конфигурисани за пријаву у два корака (према 2fa.directory). Да бисте додатно заштитили ове налоге, требало би да подесите пријаву у два корака.", "placeholders": { "count": { "content": "$1", @@ -1863,7 +1863,7 @@ "message": "Пронађене изложене лозинке" }, "exposedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "message": "Пронашли смо у вашем сефу $COUNT$ предмета који садрже лозинке откривене у познатим повредама података. Требали би да их промените да бисте користили нову лозинку.", "placeholders": { "count": { "content": "$1", @@ -1900,7 +1900,7 @@ "message": "Пронађене су слабе лозинке" }, "weakPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", + "message": "Пронашли смо у вашем сефу $COUNT$ ставки са слабим лозинкама. Требали бисте их ажурирати да би користили јаче лозинке.", "placeholders": { "count": { "content": "$1", @@ -1925,7 +1925,7 @@ "message": "Пронађене поновне лозинке" }, "reusedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", + "message": "Нашли смо $COUNT$ лозинке које се поново користе у вашем сефу. Требали бисте да их промените у јединствену вредност.", "placeholders": { "count": { "content": "$1", @@ -2788,6 +2788,12 @@ "all": { "message": "Све" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Освежи" }, @@ -6481,7 +6487,7 @@ "message": "Grant access to collections by adding them to this group." }, "editGroupCollectionsRestrictionsDesc": { - "message": "You can only assign collections you manage." + "message": "Можете да доделите само колекције којима управљате." }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." @@ -7962,7 +7968,7 @@ } }, "deleteProviderWarningDescription": { - "message": "You must unlink all clients before you can delete $ID$.", + "message": "Морате прекинути везу са свим клијентима да бисте могли да избришете $ID$.", "placeholders": { "id": { "content": "$1", @@ -8074,6 +8080,6 @@ "message": "Изаберите ставку колекције" }, "manageBillingFromProviderPortalMessage": { - "message": "Manage billing from the Provider Portal" + "message": "Управљајте наплатом из Provider Portal" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index ecc9093748..ce0ceac240 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 1d082c5eab..871f27ec7f 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Alla" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Uppdatera" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 417c7adc6a..8c37b919af 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index dd519daa29..a545446110 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 2954942728..6d22e31a5c 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Tümü" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Yenile" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 23aa9b94b9..bf7c2b746d 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "Усі" }, + "addAccess": { + "message": "Додати доступ" + }, + "addAccessFilter": { + "message": "Додати фільтр доступу" + }, "refresh": { "message": "Оновити" }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index aa5ad2c9d3..806c98e9c1 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 8e5a9e696b..5ff0721b44 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "全部" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "刷新" }, diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index c2b0060ac6..781d2ed540 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "全部" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "重新整理" }, From e2821eda4df4194f03c4ca3c316b5de4f448df4d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 12:31:31 +0000 Subject: [PATCH 06/33] Autosync the updated translations (#9111) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/es/messages.json | 30 +++++++++++------------ apps/desktop/src/locales/sr/messages.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index f7df93bdd7..d509a91d95 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -479,7 +479,7 @@ "message": "El tamaño máximo de archivo es de 500MB." }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "Se requiere migración de la clave de cifrado. Por favor, inicia sesión a través de la caja fuerte web para actualizar su clave de cifrado." }, "editedFolder": { "message": "Carpeta editada" @@ -561,10 +561,10 @@ "message": "¡Tu nueva cuenta ha sido creada! Ahora puedes acceder." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Has iniciado sesión correctamente" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Puedes cerrar esta ventana" }, "masterPassSent": { "message": "Te hemos enviado un correo electrónico con la pista de tu contraseña maestra." @@ -801,10 +801,10 @@ "message": "Cambiar contraseña maestra" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "¿Continuar a la aplicación web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Puedes cambiar tu contraseña maestra en la aplicación web de Bitwarden." }, "fingerprintPhrase": { "message": "Frase de huella digital", @@ -1090,7 +1090,7 @@ "message": "1GB de espacio en disco cifrado." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opciones de inicio de sesión con autenticación de dos pasos propietarios como YubiKey y Duo." }, "premiumSignUpReports": { "message": "Higiene de contraseña, salud de la cuenta e informes de violaciones de datos para mantener tu caja fuerte segura." @@ -1402,7 +1402,7 @@ "message": "Código PIN inválido." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Demasiados intentos de entrada de PIN no válidos. Cerrando sesión." }, "unlockWithWindowsHello": { "message": "Desbloquear con Windows Hello" @@ -1553,11 +1553,11 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Tu organización requiere que establezcas una contraseña maestra.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Verificación requerida", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1633,10 +1633,10 @@ "message": "La integración con el navegador no está soportada" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Error al habilitar la integración del navegador" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Se ha producido un error mientras se habilitaba la integración del navegador." }, "browserIntegrationMasOnlyDesc": { "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de la Mac App Store." @@ -1654,7 +1654,7 @@ "message": "Requiere una capa adicional de seguridad mediante el solicitar la frase de validación de huella dactilar al establecer un enlace entre el escritorio y el navegador. Cuando se activa, requiere intervención del usuario y verificación cada vez que se establece una conexión." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Utilizar aceleración de hardware" }, "enableHardwareAccelerationDesc": { "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." @@ -1898,16 +1898,16 @@ "message": "Su contraseña maestra no cumple con una o más de las políticas de su organización. Para acceder a la caja fuerte, debe actualizar su contraseña maestra ahora. Proceder le desconectará de su sesión actual, requiriendo que vuelva a iniciar sesión. Las sesiones activas en otros dispositivos pueden seguir estando activas durante hasta una hora." }, "tryAgain": { - "message": "Try again" + "message": "Intentar de nuevo" }, "verificationRequiredForActionSetPinToContinue": { "message": "Verification required for this action. Set a PIN to continue." }, "setPin": { - "message": "Set PIN" + "message": "Establecer PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificar biométricamente" }, "awaitingConfirmation": { "message": "Awaiting confirmation" diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 04b7e4cf29..f947d7dae7 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Успех" }, "troubleshooting": { "message": "Решавање проблема" From 800892e1f38402a6add83fd959ce02ddeb732916 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 12:39:47 +0000 Subject: [PATCH 07/33] Autosync the updated translations (#9113) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 18 +++ apps/browser/src/_locales/az/messages.json | 22 +++- apps/browser/src/_locales/be/messages.json | 18 +++ apps/browser/src/_locales/bg/messages.json | 22 +++- apps/browser/src/_locales/bn/messages.json | 18 +++ apps/browser/src/_locales/bs/messages.json | 18 +++ apps/browser/src/_locales/ca/messages.json | 18 +++ apps/browser/src/_locales/cs/messages.json | 18 +++ apps/browser/src/_locales/cy/messages.json | 18 +++ apps/browser/src/_locales/da/messages.json | 24 +++- apps/browser/src/_locales/de/messages.json | 20 ++- apps/browser/src/_locales/el/messages.json | 18 +++ apps/browser/src/_locales/en_GB/messages.json | 18 +++ apps/browser/src/_locales/en_IN/messages.json | 18 +++ apps/browser/src/_locales/es/messages.json | 20 ++- apps/browser/src/_locales/et/messages.json | 18 +++ apps/browser/src/_locales/eu/messages.json | 18 +++ apps/browser/src/_locales/fa/messages.json | 18 +++ apps/browser/src/_locales/fi/messages.json | 18 +++ apps/browser/src/_locales/fil/messages.json | 18 +++ apps/browser/src/_locales/fr/messages.json | 20 ++- apps/browser/src/_locales/gl/messages.json | 120 ++++++++++-------- apps/browser/src/_locales/he/messages.json | 18 +++ apps/browser/src/_locales/hi/messages.json | 18 +++ apps/browser/src/_locales/hr/messages.json | 18 +++ apps/browser/src/_locales/hu/messages.json | 18 +++ apps/browser/src/_locales/id/messages.json | 18 +++ apps/browser/src/_locales/it/messages.json | 18 +++ apps/browser/src/_locales/ja/messages.json | 18 +++ apps/browser/src/_locales/ka/messages.json | 18 +++ apps/browser/src/_locales/km/messages.json | 18 +++ apps/browser/src/_locales/kn/messages.json | 18 +++ apps/browser/src/_locales/ko/messages.json | 18 +++ apps/browser/src/_locales/lt/messages.json | 18 +++ apps/browser/src/_locales/lv/messages.json | 18 +++ apps/browser/src/_locales/ml/messages.json | 18 +++ apps/browser/src/_locales/mr/messages.json | 18 +++ apps/browser/src/_locales/my/messages.json | 18 +++ apps/browser/src/_locales/nb/messages.json | 18 +++ apps/browser/src/_locales/ne/messages.json | 18 +++ apps/browser/src/_locales/nl/messages.json | 18 +++ apps/browser/src/_locales/nn/messages.json | 18 +++ apps/browser/src/_locales/or/messages.json | 18 +++ apps/browser/src/_locales/pl/messages.json | 18 +++ apps/browser/src/_locales/pt_BR/messages.json | 18 +++ apps/browser/src/_locales/pt_PT/messages.json | 18 +++ apps/browser/src/_locales/ro/messages.json | 18 +++ apps/browser/src/_locales/ru/messages.json | 22 +++- apps/browser/src/_locales/si/messages.json | 18 +++ apps/browser/src/_locales/sk/messages.json | 18 +++ apps/browser/src/_locales/sl/messages.json | 18 +++ apps/browser/src/_locales/sr/messages.json | 18 +++ apps/browser/src/_locales/sv/messages.json | 18 +++ apps/browser/src/_locales/te/messages.json | 18 +++ apps/browser/src/_locales/th/messages.json | 18 +++ apps/browser/src/_locales/tr/messages.json | 22 +++- apps/browser/src/_locales/uk/messages.json | 22 +++- apps/browser/src/_locales/vi/messages.json | 18 +++ apps/browser/src/_locales/zh_CN/messages.json | 18 +++ apps/browser/src/_locales/zh_TW/messages.json | 18 +++ apps/browser/store/locales/az/copy.resx | 61 +++++---- apps/browser/store/locales/bg/copy.resx | 6 +- apps/browser/store/locales/da/copy.resx | 58 ++++----- apps/browser/store/locales/gl/copy.resx | 6 +- apps/browser/store/locales/ru/copy.resx | 6 +- apps/browser/store/locales/uk/copy.resx | 58 ++++----- 66 files changed, 1243 insertions(+), 166 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 6c83d771e9..42c1614022 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -374,12 +374,21 @@ "other": { "message": "الأخرى" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "أعدنّ طريقة إلغاء القُفْل لتغيير إجراء مهلة المخزن الخاص بك." }, "unlockMethodNeeded": { "message": "إعداد طريقة إلغاء القفل في الإعدادات" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "قيِّم هذه الإضافة" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 18fc6acca8..6ac5c64ddd 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Parol Meneceri", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Bitwarden evdə və ya işdə olarkən bütün parol, keçid açarı və həssas məlumatlarınızı asanlıqla qoruyur", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -374,12 +374,21 @@ "other": { "message": "Digər" }, + "unlockMethods": { + "message": "Kilid açma seçimləri" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Anbar vaxt bitməsi əməliyyatınızı dəyişdirmək üçün bir kilid açma üsulu qurun." }, "unlockMethodNeeded": { "message": "Ayarlarda bir kilid açma üsulu qurun" }, + "sessionTimeoutHeader": { + "message": "Seans vaxt bitməsi" + }, + "otherOptions": { + "message": "Digər seçimlər" + }, "rateExtension": { "message": "Uzantını qiymətləndir" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Konsolu" }, + "accountSecurity": { + "message": "Hesab güvənliyi" + }, + "notifications": { + "message": "Bildirişlər" + }, + "appearance": { + "message": "Görünüş" + }, "errorAssigningTargetCollection": { "message": "Hədəf kolleksiyaya təyin etmə xətası." }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 44dc85d2b9..fb99a0a95d 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Iншае" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Наладзіць метад разблакіроўкі для змянення дзеяння часу чакання вашага сховішча." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Ацаніць пашырэнне" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index cb12459a44..609131a66a 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3,11 +3,11 @@ "message": "Битуорден (Bitwarden)" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden — управител на пароли", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "У дома, на работа или на път – Битуорден защитава всички Ваши пароли, секретни ключове и лична информация", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -374,12 +374,21 @@ "other": { "message": "Други" }, + "unlockMethods": { + "message": "Настройки за отключване" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Задайте метод за отключване, за да може да промените действието при изтичане на времето за достъп до трезора." }, "unlockMethodNeeded": { "message": "Задайте метод за отключване в Настройките" }, + "sessionTimeoutHeader": { + "message": "Изтичане на времето за сесията" + }, + "otherOptions": { + "message": "Други настройки" + }, "rateExtension": { "message": "Оценяване на разширението" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Административна конзола" }, + "accountSecurity": { + "message": "Защита на регистрацията" + }, + "notifications": { + "message": "Известия" + }, + "appearance": { + "message": "Външен вид" + }, "errorAssigningTargetCollection": { "message": "Грешка при задаването на целева колекция." }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 8156e3c6f1..c0ccd92d7a 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -374,12 +374,21 @@ "other": { "message": "অন্যান্য" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "এক্সটেনশনটি মূল্যায়ন করুন" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index a7334319b5..53c5c4ed44 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index d9cd14102e..2e282e30ee 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Altres" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Configura un mètode de desbloqueig per canviar l'acció del temps d'espera de la caixa forta." }, "unlockMethodNeeded": { "message": "Configura un mètode de desbloqueig a Configuració" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Valora aquesta extensió" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Consola d'administració" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "S'ha produït un error en assignar la col·lecció de destinació." }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 058378ff17..794e2d995d 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Ostatní" }, + "unlockMethods": { + "message": "Volby odemknutí" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Nastavte metodu odemknutí, abyste změnili časový limit Vašeho trezoru." }, "unlockMethodNeeded": { "message": "Nastavit metodu odemknutí v Nastavení" }, + "sessionTimeoutHeader": { + "message": "Časový limit relace" + }, + "otherOptions": { + "message": "Další volby" + }, "rateExtension": { "message": "Ohodnotit rozšíření" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Konzole správce" }, + "accountSecurity": { + "message": "Zabezpečení účtu" + }, + "notifications": { + "message": "Oznámení" + }, + "appearance": { + "message": "Vzhled" + }, "errorAssigningTargetCollection": { "message": "Chyba při přiřazování cílové kolekce." }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 5ec7b9f483..d43cb2b25d 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Gosodiadau eraill" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rhoi eich barn ar yr estyniad" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 4c90522e1e..57f87d7a8e 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Adgangskodehåndtering", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Hjemme, på arbejde eller på farten sikrer Bitwarden nemt alle adgangskoder, adgangskort og sensitive oplysninger", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -374,12 +374,21 @@ "other": { "message": "Andre" }, + "unlockMethods": { + "message": "Oplåsningsmuligheder" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Opsæt en oplåsningsmetode til at ændre bokstimeouthandlingen." }, "unlockMethodNeeded": { "message": "Opsæt en oplåsningsmetode i Indstillinger" }, + "sessionTimeoutHeader": { + "message": "Sessionstimeout" + }, + "otherOptions": { + "message": "Andre innstillinger" + }, "rateExtension": { "message": "Bedøm udvidelsen" }, @@ -2390,7 +2399,7 @@ "message": "Generelt" }, "display": { - "message": "Display" + "message": "Skærm" }, "accountSuccessfullyCreated": { "message": "Konto oprettet!" @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin-konsol" }, + "accountSecurity": { + "message": "Kontosikkerhed" + }, + "notifications": { + "message": "Notifikationer" + }, + "appearance": { + "message": "Udseende" + }, "errorAssigningTargetCollection": { "message": "Fejl ved tildeling af målsamling." }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 35100a77d2..58e0f17053 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden Passwörter, Passkeys und vertrauliche Informationen", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -374,12 +374,21 @@ "other": { "message": "Sonstige" }, + "unlockMethods": { + "message": "Entsperroptionen" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Richte eine Entsperrmethode ein, um deine Aktion bei Timeout-Timeout zu ändern." }, "unlockMethodNeeded": { "message": "Lege eine Entsperrmethode in den Einstellungen fest" }, + "sessionTimeoutHeader": { + "message": "Sitzungs-Timeout" + }, + "otherOptions": { + "message": "Andere Optionen" + }, "rateExtension": { "message": "Erweiterung bewerten" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Administrator-Konsole" }, + "accountSecurity": { + "message": "Kontosicherheit" + }, + "notifications": { + "message": "Benachrichtigungen" + }, + "appearance": { + "message": "Aussehen" + }, "errorAssigningTargetCollection": { "message": "Fehler beim Zuweisen der Ziel-Sammlung." }, diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index de0cfb3f6c..60659eeb65 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Άλλες" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου θησαυ/κιου." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Βαθμολογήστε την επέκταση" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index a2ba76b3a6..bbaf77b714 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index a95f11ffa1..9e154437ff 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 4ffbb147be..d460a54ac8 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "En casa, el trabajo o el viaje, Bitwarden asegura todas sus contraseñas, claves e información confidencial", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -374,12 +374,21 @@ "other": { "message": "Otros" }, + "unlockMethods": { + "message": "Opciones de desbloqueo" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Configura un método de desbloqueo para cambiar tu acción de cierre de la bóveda." }, "unlockMethodNeeded": { "message": "Configure un método de desbloqueo en Configuración" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Otras opciones" + }, "rateExtension": { "message": "Valora la extensión" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Consola de administrador" }, + "accountSecurity": { + "message": "Seguridad de la cuenta" + }, + "notifications": { + "message": "Notificaciones" + }, + "appearance": { + "message": "Apariencia" + }, "errorAssigningTargetCollection": { "message": "Error al asignar la colección de destino." }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 0f5b164717..40b127f059 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Muu" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Hoidla ajalõpu tegevuse muutmiseks vali esmalt lahtilukustamise meetod." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Hinda seda laiendust" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 4bc1373201..0d4790b271 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Bestelakoak" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Baloratu gehigarria" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 8b09cb224f..6620f33c1e 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -374,12 +374,21 @@ "other": { "message": "ساير" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "یک روش بازگشایی برای پایان زمان مجاز تنظیم کنید." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "به این افزونه امتیاز دهید" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 95c77e0c09..7af0c95a1d 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Muut" }, + "unlockMethods": { + "message": "Avausasetukset" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Muuta holvisi aikakatkaisutoimintoa määrittämällä lukituksen avaustapa." }, "unlockMethodNeeded": { "message": "Määritä avaustapa asetuksista" }, + "sessionTimeoutHeader": { + "message": "Istunnon aikakatkaisu" + }, + "otherOptions": { + "message": "Muut asetukset" + }, "rateExtension": { "message": "Arvioi laajennus" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Hallintapaneelista" }, + "accountSecurity": { + "message": "Tilin suojaus" + }, + "notifications": { + "message": "Ilmoitukset" + }, + "appearance": { + "message": "Ulkoasu" + }, "errorAssigningTargetCollection": { "message": "Virhe määritettäessä kohdekokoelmaa." }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index b536fa9c19..6e52afd7db 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Iba pa" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Mag-set up ng paraan ng pag-unlock upang baguhin ang iyong pagkilos sa pag-timeout ng vault." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "I-rate ang extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 541541ab54..d53961be29 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Autre" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Configurez une méthode de déverrouillage pour changer le délai d'expiration de votre coffre." }, "unlockMethodNeeded": { "message": "Configurer une méthode de déverrouillage dans les Paramètres" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Noter l'extension" }, @@ -3010,7 +3019,7 @@ "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." }, "unassignedItemsBannerCTAPartOne": { "message": "Ajouter ces éléments à une collection depuis la", @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Console Admin" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index d23f0377bc..0c2ce6114a 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3,15 +3,15 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Xestor de contrasinais", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Na casa, no traballo ou en ruta, Bitwarden protexe os teus contrasinais, chaves de acceso e datos sensíbeis", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { - "message": "Log in or create a new account to access your secure vault." + "message": "Rexístrate ou crea unha nova conta para acceder á túa caixa forte." }, "createAccount": { "message": "Crea unha conta" @@ -20,7 +20,7 @@ "message": "Iniciar sesión" }, "enterpriseSingleSignOn": { - "message": "Enterprise single sign-on" + "message": "Inicio de sesión único empresarial" }, "cancel": { "message": "Cancelar" @@ -29,7 +29,7 @@ "message": "Pechar" }, "submit": { - "message": "Submit" + "message": "Enviar" }, "emailAddress": { "message": "Enderezo de correo electrónico" @@ -38,142 +38,142 @@ "message": "Contrasinal mestre" }, "masterPassDesc": { - "message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it." + "message": "O contrasinal mestre é a chave que empregas para acceder á túa caixa forte. É moi importante que non esquezas o teu contrasinal mestre. Non hai xeito de recuperala se a esqueces." }, "masterPassHintDesc": { - "message": "A master password hint can help you remember your password if you forget it." + "message": "Unha pista do contrasinal mestre pode axudarte a lembrar o teu contrasinal se o esqueces." }, "reTypeMasterPass": { - "message": "Re-type master password" + "message": "Reescriba o contrasinal mestre" }, "masterPassHint": { - "message": "Master password hint (optional)" + "message": "Pista do contrasinal mestre (opcional)" }, "tab": { - "message": "Tab" + "message": "Separador" }, "vault": { - "message": "Vault" + "message": "Caixa forte" }, "myVault": { - "message": "My vault" + "message": "A miña caixa forte" }, "allVaults": { - "message": "All vaults" + "message": "Todas as caixas fortes" }, "tools": { - "message": "Tools" + "message": "Ferramentas" }, "settings": { - "message": "Settings" + "message": "Axustes" }, "currentTab": { - "message": "Current tab" + "message": "Separador actual" }, "copyPassword": { - "message": "Copy password" + "message": "Copiar contrasinal" }, "copyNote": { - "message": "Copy note" + "message": "Copiar nota" }, "copyUri": { - "message": "Copy URI" + "message": "Copiar URI" }, "copyUsername": { - "message": "Copy username" + "message": "Copiar nome de usuario" }, "copyNumber": { - "message": "Copy number" + "message": "Copiar número" }, "copySecurityCode": { - "message": "Copy security code" + "message": "Copiar código de seguranza" }, "autoFill": { - "message": "Auto-fill" + "message": "Auto-encher" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Encher automaticamente inicio de sesión" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Encher automaticamente tarxeta" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Encher automaticamente identidade" }, "generatePasswordCopied": { - "message": "Generate password (copied)" + "message": "Xerar contrasinal (copiado)" }, "copyElementIdentifier": { - "message": "Copy custom field name" + "message": "Copiar nome de campo personalizado" }, "noMatchingLogins": { - "message": "No matching logins" + "message": "Sen inicios de sesión coincidentes" }, "noCards": { - "message": "No cards" + "message": "Sen tarxetas" }, "noIdentities": { - "message": "No identities" + "message": "Sen identidades" }, "addLoginMenu": { - "message": "Add login" + "message": "Engadir inicio de sesión" }, "addCardMenu": { - "message": "Add card" + "message": "Engadir tarxeta" }, "addIdentityMenu": { - "message": "Add identity" + "message": "Engadir identidade" }, "unlockVaultMenu": { - "message": "Unlock your vault" + "message": "Desbloquear a súa caixa forte" }, "loginToVaultMenu": { - "message": "Log in to your vault" + "message": "Rexistrarse na súa caixa forte" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "Non hai inicios de sesión dispoñíbeis para encher automaticamente para o separador actual do navegador." }, "addLogin": { - "message": "Add a login" + "message": "Engadir inicio de sesión" }, "addItem": { - "message": "Add item" + "message": "Engadir elemento" }, "passwordHint": { - "message": "Password hint" + "message": "Pista do contrasinal" }, "enterEmailToGetHint": { - "message": "Enter your account email address to receive your master password hint." + "message": "Introduce a dirección de correo da túa conta para recibir a pista do contrasinal mestre." }, "getMasterPasswordHint": { - "message": "Get master password hint" + "message": "Obter pista do contrasinal mestre" }, "continue": { - "message": "Continue" + "message": "Continuar" }, "sendVerificationCode": { - "message": "Send a verification code to your email" + "message": "Envía un código de verificación ao teu correo" }, "sendCode": { - "message": "Send code" + "message": "Enviar código" }, "codeSent": { - "message": "Code sent" + "message": "Código enviado" }, "verificationCode": { - "message": "Verification code" + "message": "Código de verificación" }, "confirmIdentity": { - "message": "Confirm your identity to continue." + "message": "Confirma a túa identidade para continuar." }, "account": { - "message": "Account" + "message": "Conta" }, "changeMasterPassword": { - "message": "Change master password" + "message": "Cambiar o contrasinal mestre" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continuar á aplicación web?" }, "changeMasterPasswordOnWebConfirmation": { "message": "You can change your master password on the Bitwarden web app." @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 0a9d8a8106..d8b9b1ec9e 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -374,12 +374,21 @@ "other": { "message": "אחר" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "דירוג הרחבה" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 042ef96ea2..8b08a8a5c2 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -374,12 +374,21 @@ "other": { "message": "अन्य" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the Extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 09388672ff..d00e32b139 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Ostalo" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Za promjenu vremena isteka trezora, odredi način otključavanja." }, "unlockMethodNeeded": { "message": "Postavi način otključavanja u Postavkama" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Ocijeni proširenje" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 5647f5d97d..65342fa51b 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Egyéb" }, + "unlockMethods": { + "message": "Feloldási opciók" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Állítsunk be egy feloldási módot a széf időkifutási műveletének módosításához." }, "unlockMethodNeeded": { "message": "Feloldási mód beállítása a Beállításokban" }, + "sessionTimeoutHeader": { + "message": "Munkamenet időkifutás" + }, + "otherOptions": { + "message": "Egyéb opciók" + }, "rateExtension": { "message": "Bővítmény értékelése" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Adminisztrátori konzol" }, + "accountSecurity": { + "message": "Fiókbiztonság" + }, + "notifications": { + "message": "Értesítések" + }, + "appearance": { + "message": "Megjelenés" + }, "errorAssigningTargetCollection": { "message": "Hiba történt a célgyűjtemény hozzárendelése során." }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 37961ba9a3..d0c317a177 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Lainnya" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Nilai Ekstensi" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 9420bca6ef..94e0e47915 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Altro" }, + "unlockMethods": { + "message": "Opzioni di sblocco" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Imposta un metodo di sblocco per modificare l'azione timeout cassaforte." }, "unlockMethodNeeded": { "message": "Imposta un metodo di sblocco in Impostazioni" }, + "sessionTimeoutHeader": { + "message": "Timeout della sessione" + }, + "otherOptions": { + "message": "Altre opzioni" + }, "rateExtension": { "message": "Valuta l'estensione" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Console di amministrazione" }, + "accountSecurity": { + "message": "Sicurezza dell'account" + }, + "notifications": { + "message": "Notifiche" + }, + "appearance": { + "message": "Aspetto" + }, "errorAssigningTargetCollection": { "message": "Errore nell'assegnazione della raccolta di destinazione." }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index d1429a40e0..0bfe8a84b3 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -374,12 +374,21 @@ "other": { "message": "その他" }, + "unlockMethods": { + "message": "ロック解除オプション" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "保管庫のタイムアウト動作を変更するには、ロック解除方法を設定してください。" }, "unlockMethodNeeded": { "message": "設定でロック解除方法をセットアップ" }, + "sessionTimeoutHeader": { + "message": "セッションタイムアウト" + }, + "otherOptions": { + "message": "その他のオプション" + }, "rateExtension": { "message": "拡張機能の評価" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "管理コンソール" }, + "accountSecurity": { + "message": "アカウントのセキュリティ" + }, + "notifications": { + "message": "通知" + }, + "appearance": { + "message": "外観" + }, "errorAssigningTargetCollection": { "message": "ターゲットコレクションの割り当てに失敗しました。" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 416018cc92..1ee37ad5de 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -374,12 +374,21 @@ "other": { "message": "სხვა" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index fec6ab713c..b45fdef39e 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index f5e3b8d334..275723e0cf 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -374,12 +374,21 @@ "other": { "message": "ಇತರೆ" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "ವಿಸ್ತರಣೆಯನ್ನು ರೇಟ್ ಮಾಡಿ" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 8b34a76833..116c87a311 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -374,12 +374,21 @@ "other": { "message": "기타" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "잠금 해제 방법을 설정하여 보관함의 시간 초과 동작을 변경하세요." }, "unlockMethodNeeded": { "message": "설정에서 잠금 해제 수단 설정하기" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "확장 프로그램 평가" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 6ffbf522d3..c160ae5788 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Kita" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Nustatyk atrakinimo būdą, kad pakeistum saugyklos laiko limito veiksmą." }, "unlockMethodNeeded": { "message": "Nustatykite nustatymuose atrakinimo metodą" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Įvertinkite šį plėtinį" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Administratoriaus konsolės" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Klaida priskiriant tikslinę kolekciją." }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index d8e42a1c50..c7ec65403d 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Cits" }, + "unlockMethods": { + "message": "Atslēgšanas iespējas" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Jāuzstāda atslēgšanas veids, lai mainītu glabātavas noildzes darbību." }, "unlockMethodNeeded": { "message": "Jāuzstāda atslēgšanas veids iestatījumos" }, + "sessionTimeoutHeader": { + "message": "Sesijas noildze" + }, + "otherOptions": { + "message": "Citas iespējas" + }, "rateExtension": { "message": "Novērtēt paplašinājumu" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "pārvaldības konsolē," }, + "accountSecurity": { + "message": "Konta drošība" + }, + "notifications": { + "message": "Paziņojumi" + }, + "appearance": { + "message": "Izskats" + }, "errorAssigningTargetCollection": { "message": "Kļūda mērķa krājuma piešķiršanā." }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index aa4fea96e3..0d7049b855 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -374,12 +374,21 @@ "other": { "message": "മറ്റുള്ളവ" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "എക്സ്റ്റൻഷൻ റേറ്റ് ചെയ്യുക " }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 463309789d..f44649d1c3 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -374,12 +374,21 @@ "other": { "message": "इतर" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "विस्तारकाचे मूल्यांकन करा" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index fec6ab713c..b45fdef39e 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index ea7b939f63..8d54140b0e 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Annet" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Gi denne utvidelsen en vurdering" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index fec6ab713c..b45fdef39e 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index aa01ef2c67..0319dea3a4 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Overig" }, + "unlockMethods": { + "message": "Ontgrendelopties" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Stel een ontgrendelingsmethode in om je kluis time-out actie te wijzigen." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Sessietime-out" + }, + "otherOptions": { + "message": "Andere opties" + }, "rateExtension": { "message": "Deze extensie beoordelen" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Accountbeveiliging" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Voorkomen" + }, "errorAssigningTargetCollection": { "message": "Fout bij toewijzen doelverzameling." }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index fec6ab713c..b45fdef39e 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index fec6ab713c..b45fdef39e 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 83e19315e8..b34ce5bf66 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Inne" }, + "unlockMethods": { + "message": "Odblokuj Opcje" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Ustaw metodę odblokowania, aby zmienić czas blokowania sejfu." }, "unlockMethodNeeded": { "message": "Ustaw metodę odblokowania w Ustawieniach" }, + "sessionTimeoutHeader": { + "message": "Limit czasu sesji" + }, + "otherOptions": { + "message": "Pozostałe opcje" + }, "rateExtension": { "message": "Oceń rozszerzenie" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Konsola Administracyjna" }, + "accountSecurity": { + "message": "Bezpieczeństwo konta" + }, + "notifications": { + "message": "Powiadomienia" + }, + "appearance": { + "message": "Wygląd" + }, "errorAssigningTargetCollection": { "message": "Wystąpił błąd podczas przypisywania kolekcji." }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 207c890130..c9ade42eb5 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Outros" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Configure um método de desbloqueio para alterar o tempo limite do cofre." }, "unlockMethodNeeded": { "message": "Configure um método de desbloqueio nas Configurações" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Avaliar a Extensão" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Painel de administração" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Erro ao atribuir coleção de destino." }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 26828b348a..3cb55214d6 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Outros" }, + "unlockMethods": { + "message": "Opções de desbloqueio" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Configure um método de desbloqueio para alterar a ação de tempo limite do seu cofre." }, "unlockMethodNeeded": { "message": "Definir um método de desbloqueio nas Definições" }, + "sessionTimeoutHeader": { + "message": "Tempo limite da sessão" + }, + "otherOptions": { + "message": "Outras opções" + }, "rateExtension": { "message": "Avaliar a extensão" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Consola de administração" }, + "accountSecurity": { + "message": "Segurança da conta" + }, + "notifications": { + "message": "Notificações" + }, + "appearance": { + "message": "Aparência" + }, "errorAssigningTargetCollection": { "message": "Erro ao atribuir a coleção de destino." }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 8bcf1a8430..8f02fd076d 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Altele" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Configurați metoda de deblocare care să schimbe acțiunea de expirare a seifului." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Evaluare extensie" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 23d69f44ba..8f0d211056 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Менеджер паролей", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Дома, на работе или в пути - Bitwarden всегда защитит ваши пароли, passkeys и конфиденциальную информацию", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -374,12 +374,21 @@ "other": { "message": "Прочее" }, + "unlockMethods": { + "message": "Настройки разблокировки" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Настройте способ разблокировки для изменения действия по тайм-ауту хранилища." }, "unlockMethodNeeded": { "message": "Установите способ разблокировки в настройках" }, + "sessionTimeoutHeader": { + "message": "Тайм-аут сессии" + }, + "otherOptions": { + "message": "Прочие настройки" + }, "rateExtension": { "message": "Оценить расширение" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "консоли администратора" }, + "accountSecurity": { + "message": "Безопасность аккаунта" + }, + "notifications": { + "message": "Уведомления" + }, + "appearance": { + "message": "Внешний вид" + }, "errorAssigningTargetCollection": { "message": "Ошибка при назначении целевой коллекции." }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index f225e854aa..f4f13a4f55 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -374,12 +374,21 @@ "other": { "message": "වෙනත්" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "දිගුව අනුපාතය" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 7bcffc04ac..db11a30855 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Ostatné" }, + "unlockMethods": { + "message": "Možnosti odomknutia" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Nastavte metódu odomknutia, aby ste zmenili akciu pri vypršaní času trezoru." }, "unlockMethodNeeded": { "message": "Nastavte metódu odomknutia v Nastaveniach" }, + "sessionTimeoutHeader": { + "message": "Časový limit relácie" + }, + "otherOptions": { + "message": "Ďalšie možnosti" + }, "rateExtension": { "message": "Ohodnotiť rozšírenie" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Správcovská konzola" }, + "accountSecurity": { + "message": "Zabezpečenie účtu" + }, + "notifications": { + "message": "Upozornenia" + }, + "appearance": { + "message": "Vzhľad" + }, "errorAssigningTargetCollection": { "message": "Chyba pri priraďovaní cieľovej kolekcie." }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 170ee146f7..b098546de2 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Drugo" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Da spremenite časovne omejitve trezorja, nastavite metodo odklepanja." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Ocenite to razširitev" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 3827a9dedd..621bd5953f 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Остало" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Подесите метод откључавања да бисте променили радњу временског ограничења сефа." }, "unlockMethodNeeded": { "message": "Подесите метод откључавања у подешавањима" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Оцени овај додатак" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Администраторска конзола" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Грешка при додељивању циљне колекције." }, diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 0939ac0280..96776e7060 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Annat" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Ställ in en upplåsningsmetod i Inställningar" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Betygsätt tillägget" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Utseende" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index fec6ab713c..b45fdef39e 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 009685b14b..6318d27189 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -374,12 +374,21 @@ "other": { "message": "อื่น ๆ" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the Extension" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 866125dbec..e91397a6e2 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Diğer" }, + "unlockMethods": { + "message": "Kilit açma seçenekleri" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Kasa zaman aşımı eyleminizi değiştirmek için kilit açma yönteminizi ayarlayın." }, "unlockMethodNeeded": { "message": "Ayarlar'da bir kilit açma yöntemi ayarlayın" }, + "sessionTimeoutHeader": { + "message": "Oturum zaman aşımı" + }, + "otherOptions": { + "message": "Diğer seçenekler" + }, "rateExtension": { "message": "Uzantıyı değerlendirin" }, @@ -3023,10 +3032,19 @@ "adminConsole": { "message": "Yönetici Konsolu" }, + "accountSecurity": { + "message": "Hesap güvenliği" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Hedef koleksiyonu atama hatası." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Hedef klasörü atama hatası." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 919066188a..a92a68571e 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden – менеджер паролів", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Вдома, на роботі чи в дорозі, Bitwarden захищає ваші паролі, ключі доступу та конфіденційну інформацію", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -374,12 +374,21 @@ "other": { "message": "Інше" }, + "unlockMethods": { + "message": "Налаштування розблокування" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Налаштуйте спосіб розблокування, щоб змінити час очікування сховища." }, "unlockMethodNeeded": { "message": "Встановіть спосіб розблокування в налаштуваннях" }, + "sessionTimeoutHeader": { + "message": "Час очікування сеансу" + }, + "otherOptions": { + "message": "Інші налаштування" + }, "rateExtension": { "message": "Оцінити розширення" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "консолі адміністратора," }, + "accountSecurity": { + "message": "Безпека облікового запису" + }, + "notifications": { + "message": "Сповіщення" + }, + "appearance": { + "message": "Вигляд" + }, "errorAssigningTargetCollection": { "message": "Помилка призначення цільової збірки." }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index f7a0d50bd5..77ce3ea507 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Khác" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Thiết lập phương thức mở khóa để thay đổi hành động hết thời gian chờ của vault." }, "unlockMethodNeeded": { "message": "Thiết lập phương pháp mở khóa trong Cài đặt" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Đánh giá tiện ích mở rộng" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Bảng điều khiển dành cho quản trị viên" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 706bf7a851..bedd993383 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -374,12 +374,21 @@ "other": { "message": "其他" }, + "unlockMethods": { + "message": "解锁选项" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "设置一个解锁方式以更改您的密码库超时动作。" }, "unlockMethodNeeded": { "message": "在设置中设置一个解锁方式" }, + "sessionTimeoutHeader": { + "message": "会话超时" + }, + "otherOptions": { + "message": "其他选项" + }, "rateExtension": { "message": "为本扩展打分" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "管理控制台" }, + "accountSecurity": { + "message": "账户安全" + }, + "notifications": { + "message": "通知" + }, + "appearance": { + "message": "外观" + }, "errorAssigningTargetCollection": { "message": "分配目标集合时出错。" }, diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 9300289795..4519e87981 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -374,12 +374,21 @@ "other": { "message": "其他" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "設定一個解鎖方式來變更您的密碼庫逾時動作。" }, "unlockMethodNeeded": { "message": "設定中設定解鎖" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "為本套件評分" }, @@ -3023,6 +3032,15 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, + "notifications": { + "message": "Notifications" + }, + "appearance": { + "message": "Appearance" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/store/locales/az/copy.resx b/apps/browser/store/locales/az/copy.resx index 2a3d507df2..ddbc7ac23f 100644 --- a/apps/browser/store/locales/az/copy.resx +++ b/apps/browser/store/locales/az/copy.resx @@ -118,58 +118,55 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden Parol Meneceri - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Bitwarden evdə və ya işdə olarkən bütün parol, keçid açarı və həssas məlumatlarınızı asanlıqla qoruyur. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + PCMag, WIRED, The Verge, CNET, G2 və daha çoxu tərəfindən ən yaxşı parol meneceri olaraq tanındı! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +RƏQƏMSAL HƏYATINIZI GÜVƏNDƏ SAXLAYIN +Hər hesab üçün unikal, güclü parollar yaradıb saxlayaraq rəqəmsal həyatınızı güvəndə saxlatyın və məlumat pozuntularına qarşı qorunun. Hər şeyi yalnız sizin müraciət edə biləcəyiniz ucdan uca şifrələnmiş parol anbarında saxlayın. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +DATANIZA HƏR YERDƏN, HƏR CİHAZDAN, İSTƏNİLƏN VAXT MÜRACİƏT EDİN +Limitsiz parolları limitsiz cihazlarda məhdudiyyət olmadan asanlıqla idarə edin, saxlayın, güvənlə qoruyun və paylaşın. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +HƏR KƏS İNTERNETDƏ GÜVƏNDƏ QALMAQ ÜÇÜN ALƏTLƏRƏ SAHİB OLMALIDIR +Bitwarden-i heç bir reklam və ya satış datası olmadan ödənişsiz istifadə edin. Bitwarden hesab edir ki, hər kəs internetdə güvəndə qalmaq bacarığına sahib olmalıdır. Premium planlar qabaqcıl özəlliklərə müraciət təklif edir. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +BITWARDEN İLƏ KOMANDALARINIZI GÜCLƏNDİRİN +Komanda və Müəssisələr üçün planlar, professional biznes özəllikləri ilə birgə gəlir. Bəzi nümunələrə SSO inteqrasiyası, öz-özünə sahiblik, kataloq inteqrasiyası və SCIM təqdim etmə, qlobal siyasətlər, API müraciəti, olay jurnalları və daha çoxu daxildir. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +İş gücünüzün güvənliyini qorumaq və həssas məlumatları həmkarlarınızla paylaşmaq üçün Bitwarden-i istifadə edin. +Bitwarden-i seçmək üçün daha çox səbəb: +Dünya səviyyəli şifrələmə +Parollar, qabaqcıl ucdan uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləliklə datanız güvəndə və məxfi qalır. -More reasons to choose Bitwarden: +3-cü tərəf auditləri +Bitwarden, müntəzəm olaraq tanınmış təhlükəsizlik firmaları ilə hərtərəfli üçüncü tərəf təhlükəsizlik auditləri keçirir. Bu illik auditlərə Bitwarden IP-ləri, serverləri və veb tətbiqləri arasında mənbə kodu qiymətləndirmələri və nüfuz testi daxildir. -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. - -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Qabaqcıl 2FA +Giriş məlumatlarınızı üçüncü tərəf autentifikatoru, e-poçtla göndərilən kodlar və ya avadanlıq güvənlik açarı və ya keçid açarı kimi FIDO2 WebAuthn kimlik məlumatları ilə güvəndə saxlayın. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Ucdan-uca şifrələmə güvənliyini qoruyarkən və pozuntunu məhdudlaşdırarkən dataları birbaşa başqalarına göndərin. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Daxili Generator +Ziyarət etdiyiniz hər sayt üçün uzun, mürəkkəb və fərqli parollar və unikal istifadəçi adları yaradın. Əlavə məxfilik üçün e-poçt ləqəb provayderləri ilə inteqrasiya edin. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Qlobal Tərcümələr +Bitwarden tərcümələri 60-dan çox dildə mövcuddur, Crowdin vasitəsilə qlobal icma tərəfindən tərcümə edilmişdir. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Çarpaz Platforma Tətbiqləri +İstənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan Bitwarden Anbarınızdakı həssas dataları güvəndə saxlayın və paylaşın. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - +Bitwarden parollardan daha çox qoruyur +Bitwarden-nin ucdan-uca şifrələnmiş kimlik məlumatlarını idarəetmə həlləri, təşkilatlara hər şeyi, o cümlədən tərtibatçı sirrlərini və keçid açarı təcrübələrini qorumaq gücü verir. Bitwarden Sirr Meneceri və Bitwarden Passwordless.dev haqqında daha ətraflı öyrənmək üçün Bitwarden.com saytını ziyarət edin! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Bitwarden evdə və ya işdə olarkən bütün parol, keçid açarı və həssas məlumatlarınızı asanlıqla qoruyur. Anbarınıza bir neçə cihazdan eyniləşdirərək müraciət edin diff --git a/apps/browser/store/locales/bg/copy.resx b/apps/browser/store/locales/bg/copy.resx index bc08f6a107..060ca51b21 100644 --- a/apps/browser/store/locales/bg/copy.resx +++ b/apps/browser/store/locales/bg/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden — управител на пароли - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + У дома, на работа или на път – Битуорден защитава всички Ваши пароли, секретни ключове и лична информация. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + У дома, на работа или на път – Битуорден защитава всички Ваши пароли, секретни ключове и лична информация. Удобен достъп до трезора, който се синхронизира от всички устройства diff --git a/apps/browser/store/locales/da/copy.resx b/apps/browser/store/locales/da/copy.resx index 775a3edd81..928e54d8ed 100644 --- a/apps/browser/store/locales/da/copy.resx +++ b/apps/browser/store/locales/da/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden Adgangskodehåndtering - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Hjemme, på arbejde eller på farten sikrer Bitwarden nemt alle adgangskoder, adgangskort og sensitive oplysninger. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Anerkendt som den bedste adgangskodehåndtering af PCMag, WIRED, The Verge, CNET, G2 og flere! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +SIKR DIT DIGITALE LIV +Sikr dit digitale liv og vær beskyttet mod dataindbrud ved at generere og gemme unikke, stærke adgangskoder til hver konto. Vedligehold alt i en ende-til-ende krypteret adgangskodeboks, som kun du har adgang til. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +FÅ ADGANG TIL DATAENE, hvor som helst, når som helst, PÅ ENHVER ENHED +Håndtér, gem, beskyt og del nemt et ubegrænset antal adgangskoder på tværs af et ubegrænset antal enheder uden restriktioner. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +ALLE BØR HAVE VÆRKTØJERNE TIL AT KUNNE VÆRE SIKKER ONLINE +Brug Bitwarden gratis uden annoncer eller salgsdata. Bitwarden mener, at alle bør have muligheden for at forblive sikre online. Premium-abonnementer giver adgang til avancerede funktioner. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +STYRK DINE HOLD MED BITWARDEN +Abonnementer til Teams og Enterprise kommer med professionelle forretningsfunktioner, f.eks. SSO-integration, selvhosting, katalogintegration og SCIM-klargøring, globale politikker, API-adgang, hændelseslogfiler og mere. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Brug Bitwarden til at sikre arbejdsstyrken og dele sensitive oplysninger med kolleger. -More reasons to choose Bitwarden: +Flere grunde til at vælge Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Kryptering i verdensklasse +Adgangskoder er beskyttet med avanceret ende-til-ende-kryptering (AES-256 bit, saltet hashtag og PBKDF2 SHA-256), så dataene forbliver sikre og private. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Tredjepartsrevisioner +Bitwarden udfører regelmæssigt omfattende tredjeparts sikkerhedsrevisioner med velrenommerede sikkerhedsfirmaer. Disse årlige revisioner inkluderer kildekodevurderinger og penetrationstest på tværs af Bitwarden IP'er, servere og webapplikationer. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Avanceret 2FA +Sikre dit login med en tredjepartsgodkendelse, e-mailede koder eller FIDO2 WebAuthn-legitimationsoplysninger, såsom en hardwaresikkerhedsnøgle eller adgangsnøgle. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Overfør data direkte til andre, mens ende-til-ende krypteret sikkerhed opretholdes og begrænser eksponeringen. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Indbygget generator +Opret lange, komplekse og distinkte adgangskoder og unikke brugernavne til hvert websted, som besøges. Integrér med udbydere af e-mailalias for yderligere fortrolighed. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Globale oversættelser +Bitwarden-lokaliseringer findes til flere end 60 sprog, oversat af det globale fællesskab via Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Applikationer på tværs af platforme +Sikr og del følsomme data i Bitwarden Vault fra enhver webbrowser, mobilenhed eller computer-OS og mere. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden sikrer mere end blot adgangskoder +Ende-til-ende krypterede legitimationshåndteringsløsninger fra Bitwarden giver organisationer mulighed for at sikre alt, herunder udviklerhemmeligheder og adgangsnøgleoplevelser. Besøg Bitwarden.com for at læse mere om Bitwarden Secrets Manager og Bitwarden Passwordless.dev! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Hjemme, på arbejde eller på farten sikrer Bitwarden nemt alle adgangskoder, adgangskort og sensitive oplysninger. Synkroniser og få adgang til din boks fra flere enheder diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index 0fdb224988..efcbcda29e 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden – Xestor de contrasinais - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + En casa, no traballo ou mentres estás a viaxar, Bitwarden protexe doadamente todos os teus contrasinais, chaves de paso, e información sensíbel. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + En casa, no traballo ou mentres estás a viaxar, Bitwarden protexe doadamente todos os teus contrasinais, chaves de paso, e información sensíbel. Sincroniza e accede á túa caixa forte desde múltiples dispositivos diff --git a/apps/browser/store/locales/ru/copy.resx b/apps/browser/store/locales/ru/copy.resx index 212a899f76..7c9480567e 100644 --- a/apps/browser/store/locales/ru/copy.resx +++ b/apps/browser/store/locales/ru/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden - Менеджер паролей - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Дома, на работе или в пути - Bitwarden всегда защитит ваши пароли, passkeys и конфиденциальную информацию. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Дома, на работе или в пути - Bitwarden всегда защитит ваши пароли, passkeys и конфиденциальную информацию. Синхронизация и доступ к хранилищу с нескольких устройств diff --git a/apps/browser/store/locales/uk/copy.resx b/apps/browser/store/locales/uk/copy.resx index 5a7de18363..e74811a775 100644 --- a/apps/browser/store/locales/uk/copy.resx +++ b/apps/browser/store/locales/uk/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden – менеджер паролів - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Вдома, на роботі чи в дорозі, Bitwarden захищає ваші паролі, ключі доступу та конфіденційну інформацію. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Визнаний найкращим менеджером паролів за версією PCMag, WIRED, The Verge, CNET, G2 та інших! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +ЗАХИСТІТЬ СВОЄ ЦИФРОВЕ ЖИТТЯ +Убезпечте своє цифрове життя та захистіться від витоку даних, створивши та зберігши унікальні, надійні паролі для кожного облікового запису. Зберігайте всі дані в наскрізному зашифрованому сховищі паролів, доступ до якого маєте лише ви. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ОТРИМУВАТИ ДОСТУП ДО СВОЇХ ДАНИХ БУДЬ-ДЕ, БУДЬ-КОЛИ, БУДЬ НА ЯКОМУ ПРИСТРОЇ +Легко керуйте, зберігайте, захищайте та діліться необмеженою кількістю паролів на необмеженій кількості пристроїв без обмежень. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +КОЖЕН ПОВИНЕН МАТИ ІНСТРУМЕНТИ, ЩОБ ЗАЛИШАТИСЯ В БЕЗПЕЦІ В ІНТЕРНЕТІ +Користуйтеся Bitwarden безкоштовно, без реклами і без продажу даних. Bitwarden вважає, що кожен повинен мати можливість залишатися в безпеці в Інтернеті. Преміум-плани пропонують доступ до розширених функцій. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +РОЗШИРЮЙТЕ МОЖЛИВОСТІ СВОЇХ КОМАНД ЗА ДОПОМОГОЮ BITWARDEN +Плани для Команд та Підприємства містять професійні бізнес-функції. Деякі приклади включають інтеграцію SSO, самостійний хостинг, інтеграцію каталогів і забезпечення SCIM, глобальні політики, доступ до API, журнали подій і багато іншого. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Використовуйте Bitwarden, щоб захистити своїх співробітників і ділитися конфіденційною інформацією з колегами. -More reasons to choose Bitwarden: +Більше причин вибрати Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Шифрування світового класу +Паролі захищені вдосконаленим наскрізним шифруванням (біт AES-256, солоний хештег і PBKDF2 SHA-256), тому ваші дані залишаються надійно захищеними та конфіденційними. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Аудит третьої сторони +Bitwarden регулярно проводить комплексні сторонні аудити безпеки з відомими компаніями, що займаються безпекою. Ці щорічні аудити включають оцінку вихідного коду та тестування на проникнення на всіх IP-адресах, серверах і веб-додатках Bitwarden. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Розширений 2FA +Захистіть свій вхід за допомогою стороннього автентифікатора, кодів, надісланих електронною поштою, або облікових даних FIDO2 WebAuthn, наприклад, апаратного ключа безпеки або пароля. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Передавайте дані безпосередньо іншим, зберігаючи при цьому наскрізну зашифровану безпеку та обмежуючи вразливість. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Вбудований генератор +Створюйте довгі, складні та відмінні паролі та унікальні імена користувачів для кожного сайту, який ви відвідуєте. Інтеграція з провайдерами псевдонімів електронної пошти для додаткової конфіденційності. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Глобальні переклади +Переклади Bitwarden існують для більш ніж 60 мов, перекладені світовою спільнотою за допомогою Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Крос-платформні додатки +Захищайте конфіденційні дані у своєму сховищі Bitwarden Vault та діліться ними з будь-якого браузера, мобільного пристрою, настільної операційної системи тощо. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden захищає більше, ніж просто паролі +Наскрізні рішення для управління зашифрованими обліковими даними від Bitwarden дозволяють організаціям захистити все, включаючи секрети розробників і досвід роботи з ключами. Відвідайте Bitwarden.com, щоб дізнатися більше про Bitwarden Secrets Manager і Bitwarden Passwordless.dev! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Вдома, на роботі чи в дорозі, Bitwarden захищає ваші паролі, ключі доступу та конфіденційну інформацію. Синхронізуйте й отримуйте доступ до свого сховища на різних пристроях From 0913a40447075dfa19cfddeba4731a42ae679606 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 07:16:16 -0700 Subject: [PATCH 08/33] [deps] SM: Update jest-mock-extended to v3.0.7 (#9154) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f853763efa..295206c858 100644 --- a/package-lock.json +++ b/package-lock.json @@ -152,7 +152,7 @@ "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", - "jest-mock-extended": "3.0.6", + "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", @@ -24401,12 +24401,12 @@ } }, "node_modules/jest-mock-extended": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.6.tgz", - "integrity": "sha512-DJuEoFzio0loqdX8NIwkbE9dgIXNzaj//pefOQxGkoivohpxbSQeNHCAiXkDNA/fmM4EIJDoZnSibP4w3dUJ9g==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.7.tgz", + "integrity": "sha512-7lsKdLFcW9B9l5NzZ66S/yTQ9k8rFtnwYdCNuRU/81fqDWicNDVhitTSPnrGmNeNm0xyw0JHexEOShrIKRCIRQ==", "dev": true, "dependencies": { - "ts-essentials": "^9.4.2" + "ts-essentials": "^10.0.0" }, "peerDependencies": { "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0", @@ -36344,12 +36344,12 @@ } }, "node_modules/ts-essentials": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.2.tgz", - "integrity": "sha512-mB/cDhOvD7pg3YCLk2rOtejHjjdSi9in/IBYE13S+8WA5FBSraYf4V/ws55uvs0IvQ/l0wBOlXy5yBNZ9Bl8ZQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.0.0.tgz", + "integrity": "sha512-77FHNJEyysF9+1s4G6eejuA1lxw7uMchT3ZPy3CIbh7GIunffpshtM8pTe5G6N5dpOzNevqRHew859ceLWVBfw==", "dev": true, "peerDependencies": { - "typescript": ">=4.1.0" + "typescript": ">=4.5.0" }, "peerDependenciesMeta": { "typescript": { diff --git a/package.json b/package.json index 5aca97a6f6..3ae1fb6ac7 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", - "jest-mock-extended": "3.0.6", + "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", From 9c839a62d77f453b5c65756ddd7bfedb83195cd7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 10:20:38 -0400 Subject: [PATCH 09/33] [deps] Autofill: Update tldts to v6.1.20 (#9147) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index b57b818c63..c427947bd3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -71,7 +71,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.18", + "tldts": "6.1.20", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index 295206c858..cdb0167c70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.18", + "tldts": "6.1.20", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -224,7 +224,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.18", + "tldts": "6.1.20", "zxcvbn": "4.4.2" }, "bin": { @@ -36056,20 +36056,20 @@ "dev": true }, "node_modules/tldts": { - "version": "6.1.18", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.18.tgz", - "integrity": "sha512-F+6zjPFnFxZ0h6uGb8neQWwHQm8u3orZVFribsGq4eBgEVrzSkHxzWS2l6aKr19T1vXiOMFjqfff4fQt+WgJFg==", + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.20.tgz", + "integrity": "sha512-ryfUREMx1yw2AsSMwzflHqe14DaoAoekQLjxV7gk6Uh97ng2MZdDwoxV+jTX3MpNfpwXbJuW7hriCa6/21DFNA==", "dependencies": { - "tldts-core": "^6.1.18" + "tldts-core": "^6.1.20" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.18", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.18.tgz", - "integrity": "sha512-e4wx32F/7dMBSZyKAx825Yte3U0PQtZZ0bkWxYQiwLteRVnQ5zM40fEbi0IyNtwQssgJAk3GCr7Q+w39hX0VKA==" + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.20.tgz", + "integrity": "sha512-VTEzsx7kVbLDgWaACW0atZ7Q0KzbJveYvR6IxvirIhV4Z4GGGqLVCCj9PvF0KW3h0PbJcw0JJnpr0YueHg0ueA==" }, "node_modules/tmp": { "version": "0.0.33", diff --git a/package.json b/package.json index 3ae1fb6ac7..86935bb425 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.18", + "tldts": "6.1.20", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" From a852493211ebd694aa5b3beb80177cdb5b576618 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 14 May 2024 00:22:01 +1000 Subject: [PATCH 10/33] Fix build error (#9150) --- .../vault-items/vault-collection-row.component.ts | 6 +++++- .../vault/components/vault-items/vault-items.component.html | 1 + .../vault/components/vault-items/vault-items.component.ts | 1 + apps/web/src/app/vault/org-vault/vault.component.html | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 75f2cf8322..db9648f37f 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -35,6 +35,7 @@ export class VaultCollectionRowComponent { @Input() groups: GroupView[]; @Input() showPermissionsColumn: boolean; @Input() flexibleCollectionsV1Enabled: boolean; + @Input() restrictProviderAccess: boolean; @Output() onEvent = new EventEmitter(); @@ -56,7 +57,10 @@ export class VaultCollectionRowComponent { } get permissionText() { - if (this.collection.id == Unassigned && this.organization?.canEditUnassignedCiphers()) { + if ( + this.collection.id == Unassigned && + this.organization?.canEditUnassignedCiphers(this.restrictProviderAccess) + ) { return this.i18nService.t("canEdit"); } if ((this.collection as CollectionAdminView).assigned) { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 0818ed8c1a..9939be7ddb 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -97,6 +97,7 @@ [canEditCollection]="canEditCollection(item.collection)" [canViewCollectionInfo]="canViewCollectionInfo(item.collection)" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" + [restrictProviderAccess]="restrictProviderAccess" [checked]="selection.isSelected(item)" (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 0e9ea9c5e9..f172a73b06 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -48,6 +48,7 @@ export class VaultItemsComponent { @Input({ required: true }) flexibleCollectionsV1Enabled = false; @Input() addAccessStatus: number; @Input() addAccessToggle: boolean; + @Input() restrictProviderAccess: boolean; private _ciphers?: CipherView[] = []; @Input() get ciphers(): CipherView[] { diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index f7d537de94..a437ac2092 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -71,6 +71,7 @@ [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" [addAccessStatus]="addAccessStatus$ | async" [addAccessToggle]="showAddAccessToggle" + [restrictProviderAccess]="restrictProviderAccessEnabled" > From c8c3d9d89bbddfdc8e7bf8e3e2f09f738f47b217 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 10:26:46 -0400 Subject: [PATCH 11/33] [deps] Autofill: Update rimraf to v5.0.7 (#9146) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdb0167c70..315f589632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -167,7 +167,7 @@ "react-dom": "18.3.1", "regedit": "^3.0.3", "remark-gfm": "3.0.1", - "rimraf": "5.0.5", + "rimraf": "5.0.7", "sass": "1.74.1", "sass-loader": "13.3.3", "storybook": "7.6.17", @@ -33283,9 +33283,9 @@ "dev": true }, "node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", + "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", "dev": true, "dependencies": { "glob": "^10.3.7" @@ -33294,7 +33294,7 @@ "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14" + "node": ">=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" diff --git a/package.json b/package.json index 86935bb425..53704b1860 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "react-dom": "18.3.1", "regedit": "^3.0.3", "remark-gfm": "3.0.1", - "rimraf": "5.0.5", + "rimraf": "5.0.7", "sass": "1.74.1", "sass-loader": "13.3.3", "storybook": "7.6.17", From 7a9b279b9723da0576ca1c6cf7cc287b101ca26f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 07:36:30 -0700 Subject: [PATCH 12/33] [deps] SM: Update jest-preset-angular to v14.0.4 (#9155) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 315f589632..22269986b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,7 +153,7 @@ "husky": "9.0.11", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", - "jest-preset-angular": "14.0.3", + "jest-preset-angular": "14.0.4", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", @@ -24457,9 +24457,9 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.0.3.tgz", - "integrity": "sha512-usgBL7x0rXMnMSx8iEFeOozj50W6fp+YAmQcQBUdAXhN+PAXRy4UXL6I/rfcAOU09rnnq7RKsLsmhpp/fFEuag==", + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.0.4.tgz", + "integrity": "sha512-O4WhVRdfiN9TtJMbJbuVJxD3zn6fyOF2Pqvu12fvEVR6FxCN1S1POfR2nU1fRdP+rQZv7iiW+ttxsy+qkE8iCw==", "dev": true, "dependencies": { "bs-logger": "^0.2.6", @@ -24511,9 +24511,9 @@ } }, "node_modules/jest-preset-angular/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/jest-regex-util": { diff --git a/package.json b/package.json index 53704b1860..0d0c96257d 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "husky": "9.0.11", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", - "jest-preset-angular": "14.0.3", + "jest-preset-angular": "14.0.4", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", From 7ea786e66208ff3af31325e339791f869d51a2a3 Mon Sep 17 00:00:00 2001 From: Merissa Weinstein Date: Mon, 13 May 2024 10:20:57 -0500 Subject: [PATCH 13/33] [PM-7998] update chrome extension link in the vault onboarding module (#9087) * vault-onboarding: update chrome extension link * update spec --- .../vault-onboarding/vault-onboarding.component.spec.ts | 2 +- .../vault-onboarding/vault-onboarding.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts index 8967336f75..3a72a0e4e8 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts @@ -108,7 +108,7 @@ describe("VaultOnboardingComponent", () => { it("should set extensionUrl to Chrome Web Store when isChrome is true", async () => { jest.spyOn((component as any).platformUtilsService, "isChrome").mockReturnValue(true); const expected = - "https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb"; + "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb"; await component.ngOnInit(); expect(component.extensionUrl).toEqual(expected); }); diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 90af89e60e..b7e0be2f57 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -169,7 +169,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { setInstallExtLink() { if (this.platformUtilsService.isChrome()) { this.extensionUrl = - "https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb"; + "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb"; } else if (this.platformUtilsService.isFirefox()) { this.extensionUrl = "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/"; From 5092151b546bef3679949cb47c491ab12b2fd2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Mon, 13 May 2024 12:11:15 -0400 Subject: [PATCH 14/33] add i18n messages used by forwarders (#9158) --- apps/browser/src/_locales/en/messages.json | 104 ++++++++++++++++++++- apps/cli/src/locales/en/messages.json | 102 ++++++++++++++++++++ apps/desktop/src/locales/en/messages.json | 102 ++++++++++++++++++++ apps/web/src/locales/en/messages.json | 102 ++++++++++++++++++++ 4 files changed, 409 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 9360524da1..828cca3c85 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2177,6 +2177,108 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderError": { + "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "description": "Reports an error returned by a forwarding service to the user.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Invalid characters in domain name." + } + } + }, + "forwarderGeneratedBy": { + "message": "Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen." + }, + "forwarderGeneratedByWithWebsite": { + "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen.", + "placeholders": { + "WEBSITE": { + "content": "$1", + "example": "www.example.com" + } + } + }, + "forwaderInvalidToken": { + "message": "Invalid $SERVICENAME$ API token", + "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwaderInvalidTokenWithMessage": { + "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Please verify your email address to continue." + } + } + }, + "forwarderNoAccountId": { + "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "description": "Displayed when the forwarding service fails to return an account ID.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoDomain": { + "message": "Invalid $SERVICENAME$ domain.", + "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoUrl": { + "message": "Invalid $SERVICENAME$ url.", + "description": "Displayed when the url of the forwarding service wasn't supplied.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownError": { + "message": "Unknown $SERVICENAME$ error occurred.", + "description": "Displayed when the forwarding service failed due to an unknown error.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownForwarder": { + "message": "Unknown forwarder: '$SERVICENAME$'.", + "description": "Displayed when the forwarding service is not supported.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "JustTrust.us" + } + } + }, "hostname": { "message": "Hostname", "description": "Part of a URL." @@ -3037,7 +3139,7 @@ }, "notifications": { "message": "Notifications" - }, + }, "appearance": { "message": "Appearance" }, diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 8364e0b328..3f75378112 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -58,5 +58,107 @@ }, "errorAssigningTargetFolder": { "message": "Error assigning target folder." + }, + "forwarderError": { + "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "description": "Reports an error returned by a forwarding service to the user.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Invalid characters in domain name." + } + } + }, + "forwarderGeneratedBy": { + "message": "Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen." + }, + "forwarderGeneratedByWithWebsite": { + "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen.", + "placeholders": { + "WEBSITE": { + "content": "$1", + "example": "www.example.com" + } + } + }, + "forwaderInvalidToken": { + "message": "Invalid $SERVICENAME$ API token", + "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwaderInvalidTokenWithMessage": { + "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Please verify your email address to continue." + } + } + }, + "forwarderNoAccountId": { + "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "description": "Displayed when the forwarding service fails to return an account ID.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoDomain": { + "message": "Invalid $SERVICENAME$ domain.", + "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoUrl": { + "message": "Invalid $SERVICENAME$ url.", + "description": "Displayed when the url of the forwarding service wasn't supplied.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownError": { + "message": "Unknown $SERVICENAME$ error occurred.", + "description": "Displayed when the forwarding service failed due to an unknown error.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownForwarder": { + "message": "Unknown forwarder: '$SERVICENAME$'.", + "description": "Displayed when the forwarding service is not supported.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "JustTrust.us" + } + } } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index ff9cbc97cc..9348b8dc0f 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2132,6 +2132,108 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderError": { + "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "description": "Reports an error returned by a forwarding service to the user.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Invalid characters in domain name." + } + } + }, + "forwarderGeneratedBy": { + "message": "Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen." + }, + "forwarderGeneratedByWithWebsite": { + "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen.", + "placeholders": { + "WEBSITE": { + "content": "$1", + "example": "www.example.com" + } + } + }, + "forwaderInvalidToken": { + "message": "Invalid $SERVICENAME$ API token", + "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwaderInvalidTokenWithMessage": { + "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Please verify your email address to continue." + } + } + }, + "forwarderNoAccountId": { + "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "description": "Displayed when the forwarding service fails to return an account ID.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoDomain": { + "message": "Invalid $SERVICENAME$ domain.", + "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoUrl": { + "message": "Invalid $SERVICENAME$ url.", + "description": "Displayed when the url of the forwarding service wasn't supplied.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownError": { + "message": "Unknown $SERVICENAME$ error occurred.", + "description": "Displayed when the forwarding service failed due to an unknown error.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownForwarder": { + "message": "Unknown forwarder: '$SERVICENAME$'.", + "description": "Displayed when the forwarding service is not supported.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "JustTrust.us" + } + } + }, "hostname": { "message": "Hostname", "description": "Part of a URL." diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8d3a76c790..c626c3c80e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5808,6 +5808,108 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderError": { + "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "description": "Reports an error returned by a forwarding service to the user.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Invalid characters in domain name." + } + } + }, + "forwarderGeneratedBy": { + "message": "Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen." + }, + "forwarderGeneratedByWithWebsite": { + "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "description": "Displayed with the address on the forwarding service's configuration screen.", + "placeholders": { + "WEBSITE": { + "content": "$1", + "example": "www.example.com" + } + } + }, + "forwaderInvalidToken": { + "message": "Invalid $SERVICENAME$ API token", + "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwaderInvalidTokenWithMessage": { + "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + }, + "errormessage": { + "content": "$2", + "example": "Please verify your email address to continue." + } + } + }, + "forwarderNoAccountId": { + "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "description": "Displayed when the forwarding service fails to return an account ID.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoDomain": { + "message": "Invalid $SERVICENAME$ domain.", + "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderNoUrl": { + "message": "Invalid $SERVICENAME$ url.", + "description": "Displayed when the url of the forwarding service wasn't supplied.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownError": { + "message": "Unknown $SERVICENAME$ error occurred.", + "description": "Displayed when the forwarding service failed due to an unknown error.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "SimpleLogin" + } + } + }, + "forwarderUnknownForwarder": { + "message": "Unknown forwarder: '$SERVICENAME$'.", + "description": "Displayed when the forwarding service is not supported.", + "placeholders": { + "servicename": { + "content": "$1", + "example": "JustTrust.us" + } + } + }, "hostname": { "message": "Hostname", "description": "Part of a URL." From 1a329638e0081a239ff495912aa279dbb32aafc7 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Mon, 13 May 2024 12:17:36 -0400 Subject: [PATCH 15/33] Resolved issue where non-provider user couldn't access subscription page of managed organization (#9137) --- .../organization-subscription-cloud.component.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 198332db0c..d021c827a7 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -5,8 +5,7 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; -import { OrganizationApiKeyType, ProviderType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -70,7 +69,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private route: ActivatedRoute, private dialogService: DialogService, private configService: ConfigService, - private providerService: ProviderApiServiceAbstraction, ) {} async ngOnInit() { @@ -108,15 +106,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); - if (this.userOrg.hasProvider) { - const provider = await this.providerService.getProvider(this.userOrg.providerId); - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - this.isProviderManaged = provider.type == ProviderType.Msp && enableConsolidatedBilling; - } if (this.userOrg.canViewSubscription) { + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + + this.isProviderManaged = enableConsolidatedBilling && this.userOrg.hasProvider; + this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; + if (this.lineItems && this.lineItems.length) { this.lineItems = this.lineItems .map((item) => { From dd41bcb52e84453d61338d14b9980c164fb6d8b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 09:26:24 -0700 Subject: [PATCH 16/33] [deps] SM: Update typescript-eslint monorepo to v7.8.0 (#9156) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 115 +++++++++++++++++++--------------------------- package.json | 4 +- 2 files changed, 49 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22269986b9..08365b8200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,8 +117,8 @@ "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", + "@typescript-eslint/eslint-plugin": "7.8.0", + "@typescript-eslint/parser": "7.8.0", "@webcomponents/custom-elements": "1.6.0", "autoprefixer": "10.4.19", "base64-loader": "1.0.0", @@ -10812,16 +10812,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", - "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", + "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/type-utils": "7.7.1", - "@typescript-eslint/utils": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.3.1", @@ -10847,13 +10847,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", - "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -10874,17 +10874,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", - "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", "semver": "^7.6.0" }, "engines": { @@ -10898,26 +10898,11 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -10925,12 +10910,6 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/experimental-utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", @@ -10951,15 +10930,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", - "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", + "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4" }, "engines": { @@ -10979,13 +10958,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", - "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1" + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11080,9 +11059,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", - "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11093,13 +11072,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", - "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11276,12 +11255,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", - "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/types": "7.8.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { diff --git a/package.json b/package.json index 0d0c96257d..45c2d73cb9 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", + "@typescript-eslint/eslint-plugin": "7.8.0", + "@typescript-eslint/parser": "7.8.0", "@webcomponents/custom-elements": "1.6.0", "autoprefixer": "10.4.19", "base64-loader": "1.0.0", From 473c5311fa166f07d30abd127106e4d7f289352a Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 13 May 2024 15:56:04 -0400 Subject: [PATCH 17/33] Auth/PM-5501 - VaultTimeoutSettingsService State Provider Migration (#8604) * PM-5501 - VaultTimeoutSettingsSvc - refactor var names in getVaultTimeoutAction * PM-5501 - Add state definitions and key definitions + test deserialization of key defs. * PM-5501 - Add state provider dep to VaultTimeoutSettingsSvc * PM-5501 - Refactor getVaultTimeout * PM-5501 - VaultTimeoutSettingsService - Build getMaxVaultTimeoutPolicyByUserId helper * PM-5501 - (1) Update state definitions (2) convert KeyDefs to UserKeyDefs (2) Remove everBeenUnlocked as we won't need it * PM-5501 - VaultTimeoutSettingsSvc - POC for getVaultTimeoutActionByUserId$ method + new private determineVaultTimeoutAction helper. * PM-5501 - VaultTimeoutSettingsSvc - build set and observable get methods for vault timeout settings * PM-5501 - Update web references to use new vault timeout setting service methods * PM-5501 - VaultTimeoutSettingsSvc - write up abstraction js docs * PM-5501 - VaultTimeoutSettingsSvc abstraction - finish tweaks * PM-5501 - VaultTimeoutSettingsSvc - add catchError blocks to observables to protect outer observables and prevent cancellation in case of error. * PM-5501 - Remove vault timeout settings from state service implementation. * PM-5501 - VaultTimeoutSettingsServiceStateProviderMigrator first draft * PM-5501 - WIP - replace some state service calls with calls to vault timeout settings svc. * PM-5501 - Replace state service calls in login strategies to get vault timeout settings data with VaultTimeoutSettingsService calls. * PM-5501 - Fix login strategy tests * PM-5501 - Update login strategy tests to pass * PM-5501 - CryptoSvc - share VaultTimeout user key def to allow crypto svc access to the vault timeout without creating a circular dep. * PM-5501 - Fix dependency injections. * PM-5501 - ApiSvc - replace state svc with vault timeout settings svc. * PM-5501 - VaultTimeoutSettingsServiceStateProviderMigrator more cleanup * PM-5501 - Test VaultTimeoutSettingsServiceStateProviderMigrator * PM-5501 - VaultTimeoutSettingsSvc tests updated * PM-5501 - Update all setVaultTimeoutOptions references * PM-5501 - VaultTimeoutSettingsSvc - Update setVaultTimeoutOptions to remove unnecessary logic and clean up clearTokens condition. * PM-5501 - Fix vault timeout service tests * PM-5501 - Update VaultTimeoutSettings state tests to pass * PM-5501 - Desktop - system svc - fix build by replacing use of removed method. * PM-5501 - Fix CLI by properly configuring super class deps in NodeApiService * PM-5501 - Actually finish getitng deps fixed to get CLI to build * PM-5501 - VaultTimeoutSettingsSvc.determineVaultTimeoutAction - pass userId to getAvailableVaultTimeoutActions to prevent hang waiting for an active user. * PM-5501 - VaultTimeoutSettingSvc test - enhance getVaultTimeoutActionByUserId$ to also test PIN scenarios as an unlock method * PM-5501 - bump migration version * PM-5501 - Refactor migration to ensure the migration persists null vault timeout values. * PM-5501 - Bump migration version * PM-5501 - Fix web build issues introduced by merging main. * PM-5501 - Bump migration version * PM-5501 - PreferencesComponent - revert dep change from InternalPolicyService to standard PolicyService abstraction * PM-5501 - Address all PR feedback from Jake Co-authored-by: Jake Fink * PM-5501 - VaultTimeoutSettingsSvc tests - add tests for setVaultTimeoutOptions * PM-5501 - VaultTimeoutSettingsSvc - setVaultTimeoutOptions - Update tests to use platform's desired syntax. * PM-5501 - Fix tests * PM-5501 - Create new VaultTimeout type * PM-5501 - Create new DEFAULT_VAULT_TIMEOUT to allow each client to inject their default timeout into the VaultTimeoutSettingService * PM-5501 - Migrate client default vault timeout to new injection token * PM-5501 - Update VaultTimeoutSettingsSvc to use VaultTimeout type and apply default vault timeout if it is null. * PM-5501 - Update vaultTimeout: number to be vaultTimeout: VaultTimeout everywhere I could find it. * PM-5501 - More changes based on changing vaultTimeout from number to VaultTimeout type. * PM-5501 - VaultTimeoutSvc - Update shouldLock logic which previously checked for null (never) or any negative values (any strings except never) with a simple string type check. * PM-5501 - More cleanup of vaultTimeout type change - replacing null checks with "never" checks * PM-5501 - VaultTimeoutSettingsSvc - refactor determineVaultTimeout to properly treat string and numeric vault timeouts. * PM-5501 - Update vault timeout settings service tests to reflect new VaultTimeout type. * PM-5501 - VaultTimeoutSettingsService - add more test cases for getVaultTimeoutByUserId * PM-5501 - (1) Remove "immediately" as 0 is numerically meaningful and can be used with Math.min (2) Add VaultTimeoutOption interface for use in all places we show the user a list of vault timeout options. * PM-5501 - VaultTimeoutSettingSvc - update tests to use 0 as immediately. * PM-5501 - VaultTimeoutInputComp - Add new types and update applyVaultTimeoutPolicy logic appropriately. * PM-5501 - Add new types to all preferences and setting components across clients. * PM-5501 - Fix bug on web where navigating to the preferences page throws an error b/c the validatorChange function isn't defined. * PM-5501 - WIP on updating vault timeout setting migration and rollback + testing it. * PM-5501 - Update VaultTimeoutSettingsSvc state provider migration and tests to map existing possible values into new VaultTImeout type. * PM-5501 - Fix vault timeout settings state tests by changing number to new VaultTimeout type. * PM-5501 - Fix crypto svc auto key refresh test to use "never" instead of null. * PM-5501 - Add clarifying comment to vaulttimeout type * PM-5501 - Desktop app comp - replace systemTimeoutOptions with vault timeout type. * PM-5501 - Update vault timeout service tests to use VaultTimeout type. * PM-5501 - VaultTimeoutSettingsSvc - (1) Fix bug where vault timeout action didn't have a default like it did before (2) Fix bug in userHasMasterPassword where it would incorrectly return the active user stream for a given user id as a fallback. There is no guarantee the given user would match the active user so the paths are mutually exclusive. * PM-5501 - Login Strategy fix - Move retrieval of vault timeout settings and setting of the tokens until after account init and user decryption options set as those opts are needed to properly determine the user's available vault timeout actions. * PM-5501 - Fix vault timeout settings svc tests * PM-5501 - VaultTimeoutSettingSvc - move default logic to determine methods + refactor default vault timeout action to properly default to lock in scenarios the user has lock available. * Update libs/angular/src/components/settings/vault-timeout-input.component.ts Co-authored-by: Cesar Gonzalez * PM-5501 - Per PR feedback, cleanup commented out vault timeout options * PM-5501 - Fix vault timeout input comp lint issues * PM-5501 - Per PR feedback from Cesar, update VaultTimeout type to use const so we can avoid any magic string usage. Awesome. Co-authored-by: Cesar Gonzalez * PM-5501 - CLI - use "never" as default vault timeout instead of null. * PM-5501 - Fix broken tests * PM-5501 - Bump migration version * PM-5501 - Fix build errors after merging main. * PM-5501 - Update mockMigrationHelper to pass along client type so tests will respect it. * PM-5501 - Update VaultTimeoutSettingsServiceStateProviderMigrator and tests to use new CLI client type to convert undefined values to never so that CLI users don't lose their session upon running this migration. * PM-5501 - Bump migration version * PM-5501 - Fix migration tests to use new authenticated user format * PM-5501 Update rollback tests * PM-5501 - Adjust migration based on feedback. * PM-5501 - Per Jake's find, fix missed -2 Co-authored-by: Jake Fink * PM-5501 - Add user id to needsStorageReseed. Co-authored-by: Jake Fink * PM-5501 - Per PR feedback, setVaultTimeoutOptions shouldn't accept null for vault timeout anymore. * PM-5501 - Per PR feedback, add null checks for set methods for setting vault timeout or vault timeout action. * PM-5501 - Per PR feedback, add more context as to why we need vault timeout settings to persist after logout. * PM-5501 - Per PR feedback, fix userHasMasterPassword * PM-5501 - VaultTimeoutSettingsService - fix userHasMasterPassword check by checking for null decryption options. * PM-5501 - Remove state service from vault timeout settings service (WOOO) * PM-5501 - Bump migration version * PM-5501 - Account Security comp - refactor to consider ease of debugging. * PM-5501 - (1) Add checks for null vault timeout and vault timeout actions (2) Add tests for new scenarios. * PM-5501 - VaultTimeoutSettingsSvc - setVaultTimeoutOptions - fix bug where nullish check would throw incorrectly if immediately (0) was picked as the timeout. * PM-5501 - Per PR feedback, clean up remaining token service methods which accept null for timeout and add tests. . * PM-5501 - Fix nit --------- Co-authored-by: Jake Fink Co-authored-by: Cesar Gonzalez --- .../login-strategy-service.factory.ts | 6 + .../settings/account-security.component.ts | 54 +- apps/browser/src/autofill/types/index.ts | 3 +- .../browser/src/background/idle.background.ts | 13 +- .../browser/src/background/main.background.ts | 46 +- .../vault-timeout-settings-service.factory.ts | 20 +- apps/browser/src/models/account.ts | 24 +- .../service-factories/api-service.factory.ts | 9 +- .../src/popup/services/services.module.ts | 6 + apps/cli/src/bw.ts | 43 +- .../src/platform/services/node-api.service.ts | 6 +- .../src/app/accounts/settings.component.ts | 45 +- apps/desktop/src/app/app.component.ts | 25 +- .../src/app/services/services.module.ts | 6 + apps/desktop/src/models/account.ts | 1 - apps/web/src/app/core/core.module.ts | 8 + apps/web/src/app/core/state/account.ts | 16 +- .../src/app/settings/preferences.component.ts | 30 +- .../settings/vault-timeout-input.component.ts | 33 +- libs/angular/src/services/injection-tokens.ts | 2 + .../src/services/jslib-services.module.ts | 8 +- .../auth-request-login.strategy.spec.ts | 21 + .../auth-request-login.strategy.ts | 3 + .../login-strategies/login.strategy.spec.ts | 65 +- .../common/login-strategies/login.strategy.ts | 32 +- .../password-login.strategy.spec.ts | 21 + .../password-login.strategy.ts | 3 + .../sso-login.strategy.spec.ts | 21 + .../login-strategies/sso-login.strategy.ts | 3 + .../user-api-login.strategy.spec.ts | 24 +- .../user-api-login.strategy.ts | 11 +- .../webauthn-login.strategy.spec.ts | 22 + .../webauthn-login.strategy.ts | 3 + .../login-strategy.service.spec.ts | 21 + .../login-strategy.service.ts | 7 + .../vault-timeout-settings.service.ts | 27 +- .../src/auth/abstractions/token.service.ts | 9 +- .../src/auth/services/token.service.spec.ts | 335 ++++++++- .../common/src/auth/services/token.service.ts | 77 +- .../platform/abstractions/state.service.ts | 4 - .../src/platform/models/domain/account.ts | 2 - .../platform/models/domain/global-state.ts | 2 - .../platform/services/crypto.service.spec.ts | 8 +- .../src/platform/services/crypto.service.ts | 12 +- .../src/platform/services/state.service.ts | 43 -- .../src/platform/services/system.service.ts | 10 +- .../src/platform/state/state-definitions.ts | 7 + libs/common/src/services/api.service.ts | 28 +- .../vault-timeout-settings.service.spec.ts | 247 ++++++- .../vault-timeout-settings.service.ts | 260 +++++-- .../vault-timeout-settings.state.spec.ts | 36 + .../vault-timeout-settings.state.ts | 27 + .../vault-timeout.service.spec.ts | 25 +- .../vault-timeout/vault-timeout.service.ts | 9 +- libs/common/src/state-migrations/migrate.ts | 6 +- .../state-migrations/migration-helper.spec.ts | 1 + ...out-settings-svc-to-state-provider.spec.ts | 669 ++++++++++++++++++ ...-timeout-settings-svc-to-state-provider.ts | 174 +++++ libs/common/src/types/vault-timeout.type.ts | 17 + 59 files changed, 2306 insertions(+), 390 deletions(-) create mode 100644 libs/common/src/services/vault-timeout/vault-timeout-settings.state.spec.ts create mode 100644 libs/common/src/services/vault-timeout/vault-timeout-settings.state.ts create mode 100644 libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts create mode 100644 libs/common/src/types/vault-timeout.type.ts diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index c414300431..83ebcaa11e 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -4,6 +4,10 @@ import { policyServiceFactory, PolicyServiceInitOptions, } from "../../../admin-console/background/service-factories/policy-service.factory"; +import { + vaultTimeoutSettingsServiceFactory, + VaultTimeoutSettingsServiceInitOptions, +} from "../../../background/service-factories/vault-timeout-settings-service.factory"; import { apiServiceFactory, ApiServiceInitOptions, @@ -108,6 +112,7 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & BillingAccountProfileStateServiceInitOptions & + VaultTimeoutSettingsServiceInitOptions & KdfConfigServiceInitOptions; export function loginStrategyServiceFactory( @@ -142,6 +147,7 @@ export function loginStrategyServiceFactory( await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await vaultTimeoutSettingsServiceFactory(cache, opts), await kdfConfigServiceFactory(cache, opts), ), ); diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 51959ba03c..ea7c3a5e8d 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -31,6 +31,11 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { + VaultTimeout, + VaultTimeoutOption, + VaultTimeoutStringType, +} from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; @@ -50,7 +55,7 @@ export class AccountSecurityComponent implements OnInit { protected readonly VaultTimeoutAction = VaultTimeoutAction; availableVaultTimeoutActions: VaultTimeoutAction[] = []; - vaultTimeoutOptions: any[]; + vaultTimeoutOptions: VaultTimeoutOption[]; vaultTimeoutPolicyCallout: Observable<{ timeout: { hours: number; minutes: number }; action: VaultTimeoutAction; @@ -60,7 +65,7 @@ export class AccountSecurityComponent implements OnInit { accountSwitcherEnabled = false; form = this.formBuilder.group({ - vaultTimeout: [null as number | null], + vaultTimeout: [null as VaultTimeout | null], vaultTimeoutAction: [VaultTimeoutAction.Lock], pin: [null as boolean | null], biometric: false, @@ -118,20 +123,31 @@ export class AccountSecurityComponent implements OnInit { { name: this.i18nService.t("thirtyMinutes"), value: 30 }, { name: this.i18nService.t("oneHour"), value: 60 }, { name: this.i18nService.t("fourHours"), value: 240 }, - // { name: i18nService.t('onIdle'), value: -4 }, - // { name: i18nService.t('onSleep'), value: -3 }, ]; if (showOnLocked) { - this.vaultTimeoutOptions.push({ name: this.i18nService.t("onLocked"), value: -2 }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("onLocked"), + value: VaultTimeoutStringType.OnLocked, + }); } - this.vaultTimeoutOptions.push({ name: this.i18nService.t("onRestart"), value: -1 }); - this.vaultTimeoutOptions.push({ name: this.i18nService.t("never"), value: null }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("onRestart"), + value: VaultTimeoutStringType.OnRestart, + }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("never"), + value: VaultTimeoutStringType.Never, + }); - let timeout = await this.vaultTimeoutSettingsService.getVaultTimeout(); - if (timeout === -2 && !showOnLocked) { - timeout = -1; + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + let timeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id), + ); + if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) { + timeout = VaultTimeoutStringType.OnRestart; } this.form.controls.vaultTimeout.valueChanges @@ -159,7 +175,7 @@ export class AccountSecurityComponent implements OnInit { const initialValues = { vaultTimeout: timeout, vaultTimeoutAction: await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), ), pin: await this.pinService.isPinSet(userId), biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(), @@ -203,7 +219,7 @@ export class AccountSecurityComponent implements OnInit { switchMap(() => combineLatest([ this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), ]), ), takeUntil(this.destroy$), @@ -237,7 +253,7 @@ export class AccountSecurityComponent implements OnInit { }); } - async saveVaultTimeout(previousValue: number, newValue: number) { + async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) { if (newValue == null) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, @@ -262,9 +278,16 @@ export class AccountSecurityComponent implements OnInit { return; } + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), + ); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, newValue, - await firstValueFrom(this.vaultTimeoutSettingsService.vaultTimeoutAction$()), + vaultTimeoutAction, ); if (newValue == null) { this.messagingService.send("bgReseedStorage"); @@ -296,7 +319,10 @@ export class AccountSecurityComponent implements OnInit { return; } + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, this.form.value.vaultTimeout, newValue, ); diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index 8ed893e733..a14ef1330c 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -1,5 +1,6 @@ import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; +import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; import { CipherType } from "@bitwarden/common/vault/enums"; export type UserSettings = { @@ -31,7 +32,7 @@ export type UserSettings = { utcDate: string; version: string; }; - vaultTimeout: number; + vaultTimeout: VaultTimeout; vaultTimeoutAction: VaultTimeoutAction; }; diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 7b273459ad..eef033b364 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -1,9 +1,11 @@ import { firstValueFrom } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; @@ -19,6 +21,7 @@ export default class IdleBackground { private stateService: BrowserStateService, private notificationsService: NotificationsService, private accountService: AccountService, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, ) { this.idle = chrome.idle || (browser != null ? browser.idle : null); } @@ -54,10 +57,14 @@ export default class IdleBackground { const allUsers = await firstValueFrom(this.accountService.accounts$); for (const userId in allUsers) { // If the screen is locked or the screensaver activates - const timeout = await this.stateService.getVaultTimeout({ userId: userId }); - if (timeout === -2) { + const timeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + if (timeout === VaultTimeoutStringType.OnLocked) { // On System Lock vault timeout option - const action = await this.stateService.getVaultTimeoutAction({ userId: userId }); + const action = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); if (action === VaultTimeoutAction.LogOut) { await this.vaultTimeoutService.logOut(userId); } else { diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 6120155334..004714258e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -154,6 +154,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/vault/abstractions/collection.service"; import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/vault/abstractions/fido2/fido2-authenticator.service.abstraction"; @@ -581,12 +582,30 @@ export default class MainBackground { ); this.appIdService = new AppIdService(this.globalStateProvider); + + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.organizationService = new OrganizationService(this.stateProvider); + this.policyService = new PolicyService(this.stateProvider, this.organizationService); + + this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.accountService, + this.pinService, + this.userDecryptionOptionsService, + this.cryptoService, + this.tokenService, + this.policyService, + this.biometricStateService, + this.stateProvider, + this.logService, + VaultTimeoutStringType.OnRestart, // default vault timeout + ); + this.apiService = new ApiService( this.tokenService, this.platformUtilsService, this.environmentService, this.appIdService, - this.stateService, + this.vaultTimeoutSettingsService, (expired: boolean) => this.logout(expired), ); this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); @@ -603,8 +622,7 @@ export default class MainBackground { this.stateProvider, ); this.syncNotifierService = new SyncNotifierService(); - this.organizationService = new OrganizationService(this.stateProvider); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); + this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, this.policyService, @@ -710,17 +728,6 @@ export default class MainBackground { ); this.folderApiService = new FolderApiService(this.folderService, this.apiService); - this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( - this.accountService, - this.pinService, - this.userDecryptionOptionsService, - this.cryptoService, - this.tokenService, - this.policyService, - this.stateService, - this.biometricStateService, - ); - this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, @@ -1056,6 +1063,7 @@ export default class MainBackground { this.stateService, this.notificationsService, this.accountService, + this.vaultTimeoutSettingsService, ); this.usernameGenerationService = new UsernameGenerationService( @@ -1263,7 +1271,7 @@ export default class MainBackground { ]); //Needs to be checked before state is cleaned - const needStorageReseed = await this.needsStorageReseed(); + const needStorageReseed = await this.needsStorageReseed(userId); const newActiveUser = userBeingLoggedOut === activeUserId @@ -1307,9 +1315,11 @@ export default class MainBackground { await this.systemService.startProcessReload(this.authService); } - private async needsStorageReseed(): Promise { - const currentVaultTimeout = await this.stateService.getVaultTimeout(); - return currentVaultTimeout == null ? false : true; + private async needsStorageReseed(userId: UserId): Promise { + const currentVaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + return currentVaultTimeout == VaultTimeoutStringType.Never ? false : true; } async collectPageDetailsForContentScript(tab: any, sender: string, frameId: number = null) { diff --git a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts index 6c5ea63eba..5f98d9764c 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts @@ -1,5 +1,6 @@ import { VaultTimeoutSettingsService as AbstractVaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { policyServiceFactory, @@ -35,9 +36,13 @@ import { FactoryOptions, } from "../../platform/background/service-factories/factory-options"; import { - StateServiceInitOptions, - stateServiceFactory, -} from "../../platform/background/service-factories/state-service.factory"; + logServiceFactory, + LogServiceInitOptions, +} from "../../platform/background/service-factories/log-service.factory"; +import { + StateProviderInitOptions, + stateProviderFactory, +} from "../../platform/background/service-factories/state-provider.factory"; type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions; @@ -48,8 +53,9 @@ export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsService CryptoServiceInitOptions & TokenServiceInitOptions & PolicyServiceInitOptions & - StateServiceInitOptions & - BiometricStateServiceInitOptions; + BiometricStateServiceInitOptions & + StateProviderInitOptions & + LogServiceInitOptions; export function vaultTimeoutSettingsServiceFactory( cache: { vaultTimeoutSettingsService?: AbstractVaultTimeoutSettingsService } & CachedServices, @@ -67,8 +73,10 @@ export function vaultTimeoutSettingsServiceFactory( await cryptoServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await policyServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), await biometricStateServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), + await logServiceFactory(cache, opts), + VaultTimeoutStringType.OnRestart, // default vault timeout ), ); } diff --git a/apps/browser/src/models/account.ts b/apps/browser/src/models/account.ts index 57d7844fde..519f1bda6b 100644 --- a/apps/browser/src/models/account.ts +++ b/apps/browser/src/models/account.ts @@ -1,28 +1,12 @@ import { Jsonify } from "type-fest"; -import { - Account as BaseAccount, - AccountSettings as BaseAccountSettings, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account"; import { BrowserComponentState } from "./browserComponentState"; import { BrowserGroupingsComponentState } from "./browserGroupingsComponentState"; import { BrowserSendComponentState } from "./browserSendComponentState"; -export class AccountSettings extends BaseAccountSettings { - vaultTimeout = -1; // On Restart - - static fromJSON(json: Jsonify): AccountSettings { - if (json == null) { - return null; - } - - return Object.assign(new AccountSettings(), json, super.fromJSON(json)); - } -} - export class Account extends BaseAccount { - settings?: AccountSettings = new AccountSettings(); groupings?: BrowserGroupingsComponentState; send?: BrowserSendComponentState; ciphers?: BrowserComponentState; @@ -30,10 +14,7 @@ export class Account extends BaseAccount { constructor(init: Partial) { super(init); - Object.assign(this.settings, { - ...new AccountSettings(), - ...this.settings, - }); + this.groupings = init?.groupings ?? new BrowserGroupingsComponentState(); this.send = init?.send ?? new BrowserSendComponentState(); this.ciphers = init?.ciphers ?? new BrowserComponentState(); @@ -46,7 +27,6 @@ export class Account extends BaseAccount { } return Object.assign(new Account({}), json, super.fromJSON(json), { - settings: AccountSettings.fromJSON(json.settings), groupings: BrowserGroupingsComponentState.fromJSON(json.groupings), send: BrowserSendComponentState.fromJSON(json.send), ciphers: BrowserComponentState.fromJSON(json.ciphers), diff --git a/apps/browser/src/platform/background/service-factories/api-service.factory.ts b/apps/browser/src/platform/background/service-factories/api-service.factory.ts index 57cd500441..bfae93f3d8 100644 --- a/apps/browser/src/platform/background/service-factories/api-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/api-service.factory.ts @@ -5,6 +5,10 @@ import { tokenServiceFactory, TokenServiceInitOptions, } from "../../../auth/background/service-factories/token-service.factory"; +import { + vaultTimeoutSettingsServiceFactory, + VaultTimeoutSettingsServiceInitOptions, +} from "../../../background/service-factories/vault-timeout-settings-service.factory"; import { CachedServices, factory, @@ -20,7 +24,6 @@ import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, } from "./platform-utils-service.factory"; -import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; type ApiServiceFactoryOptions = FactoryOptions & { apiServiceOptions: { @@ -34,7 +37,7 @@ export type ApiServiceInitOptions = ApiServiceFactoryOptions & PlatformUtilsServiceInitOptions & EnvironmentServiceInitOptions & AppIdServiceInitOptions & - StateServiceInitOptions; + VaultTimeoutSettingsServiceInitOptions; export function apiServiceFactory( cache: { apiService?: AbstractApiService } & CachedServices, @@ -50,7 +53,7 @@ export function apiServiceFactory( await platformUtilsServiceFactory(cache, opts), await environmentServiceFactory(cache, opts), await appIdServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), + await vaultTimeoutSettingsServiceFactory(cache, opts), opts.apiServiceOptions.logoutCallback, opts.apiServiceOptions.customUserAgent, ), diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 2313833ab8..0b9c8f6fe6 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -12,6 +12,7 @@ import { OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, + DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; @@ -82,6 +83,7 @@ import { import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -161,6 +163,10 @@ const safeProviders: SafeProvider[] = [ safeProvider(DebounceNavigationService), safeProvider(DialogService), safeProvider(PopupCloseWarningService), + safeProvider({ + provide: DEFAULT_VAULT_TIMEOUT, + useValue: VaultTimeoutStringType.OnRestart, + }), safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => Promise>, useFactory: (initService: InitService) => initService.init(), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 2f5eefdbb2..cab38965f8 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -116,6 +116,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/services/collection.service"; @@ -403,12 +404,32 @@ export class Main { " (" + this.platformUtilsService.getDeviceString().toUpperCase() + ")"; + + this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + + this.organizationService = new OrganizationService(this.stateProvider); + this.policyService = new PolicyService(this.stateProvider, this.organizationService); + + this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.accountService, + this.pinService, + this.userDecryptionOptionsService, + this.cryptoService, + this.tokenService, + this.policyService, + this.biometricStateService, + this.stateProvider, + this.logService, + VaultTimeoutStringType.Never, // default vault timeout + ); + this.apiService = new NodeApiService( this.tokenService, this.platformUtilsService, this.environmentService, this.appIdService, - this.stateService, + this.vaultTimeoutSettingsService, async (expired: boolean) => await this.logout(), customUserAgent, ); @@ -454,12 +475,8 @@ export class Main { this.providerService = new ProviderService(this.stateProvider); - this.organizationService = new OrganizationService(this.stateProvider); - this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( @@ -489,8 +506,6 @@ export class Main { this.stateService, ); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, @@ -543,6 +558,7 @@ export class Main { this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); @@ -590,19 +606,6 @@ export class Main { const lockedCallback = async (userId?: string) => await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); - this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); - - this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( - this.accountService, - this.pinService, - this.userDecryptionOptionsService, - this.cryptoService, - this.tokenService, - this.policyService, - this.stateService, - this.biometricStateService, - ); - this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, diff --git a/apps/cli/src/platform/services/node-api.service.ts b/apps/cli/src/platform/services/node-api.service.ts index 9099fd2760..4849aef151 100644 --- a/apps/cli/src/platform/services/node-api.service.ts +++ b/apps/cli/src/platform/services/node-api.service.ts @@ -2,11 +2,11 @@ import * as FormData from "form-data"; import { HttpsProxyAgent } from "https-proxy-agent"; import * as fe from "node-fetch"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ApiService } from "@bitwarden/common/services/api.service"; (global as any).fetch = fe.default; @@ -21,7 +21,7 @@ export class NodeApiService extends ApiService { platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, appIdService: AppIdService, - stateService: StateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, logoutCallback: (expired: boolean) => Promise, customUserAgent: string = null, ) { @@ -30,7 +30,7 @@ export class NodeApiService extends ApiService { platformUtilsService, environmentService, appIdService, - stateService, + vaultTimeoutSettingsService, logoutCallback, customUserAgent, ); diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 80f634d0c5..eedf072a81 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -24,6 +24,11 @@ import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { + VaultTimeout, + VaultTimeoutOption, + VaultTimeoutStringType, +} from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; @@ -41,7 +46,7 @@ export class SettingsComponent implements OnInit { protected readonly VaultTimeoutAction = VaultTimeoutAction; showMinToTray = false; - vaultTimeoutOptions: any[]; + vaultTimeoutOptions: VaultTimeoutOption[]; localeOptions: any[]; themeOptions: any[]; clearClipboardOptions: any[]; @@ -72,14 +77,14 @@ export class SettingsComponent implements OnInit { timeout: { hours: number; minutes: number }; action: "lock" | "logOut"; }>; - previousVaultTimeout: number = null; + previousVaultTimeout: VaultTimeout = null; userHasMasterPassword: boolean; userHasPinSet: boolean; form = this.formBuilder.group({ // Security - vaultTimeout: [null as number | null], + vaultTimeout: [null as VaultTimeout | null], vaultTimeoutAction: [VaultTimeoutAction.Lock], pin: [null as boolean | null], biometric: false, @@ -159,24 +164,26 @@ export class SettingsComponent implements OnInit { this.showDuckDuckGoIntegrationOption = isMac; this.vaultTimeoutOptions = [ - // { name: i18nService.t('immediately'), value: 0 }, { name: this.i18nService.t("oneMinute"), value: 1 }, { name: this.i18nService.t("fiveMinutes"), value: 5 }, { name: this.i18nService.t("fifteenMinutes"), value: 15 }, { name: this.i18nService.t("thirtyMinutes"), value: 30 }, { name: this.i18nService.t("oneHour"), value: 60 }, { name: this.i18nService.t("fourHours"), value: 240 }, - { name: this.i18nService.t("onIdle"), value: -4 }, - { name: this.i18nService.t("onSleep"), value: -3 }, + { name: this.i18nService.t("onIdle"), value: VaultTimeoutStringType.OnIdle }, + { name: this.i18nService.t("onSleep"), value: VaultTimeoutStringType.OnSleep }, ]; if (this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop) { - this.vaultTimeoutOptions.push({ name: this.i18nService.t("onLocked"), value: -2 }); + this.vaultTimeoutOptions.push({ + name: this.i18nService.t("onLocked"), + value: VaultTimeoutStringType.OnLocked, + }); } this.vaultTimeoutOptions = this.vaultTimeoutOptions.concat([ - { name: this.i18nService.t("onRestart"), value: -1 }, - { name: this.i18nService.t("never"), value: null }, + { name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart }, + { name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never }, ]); const localeOptions: any[] = []; @@ -251,10 +258,14 @@ export class SettingsComponent implements OnInit { // Load initial values this.userHasPinSet = await this.pinService.isPinSet(userId); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + const initialValues = { - vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), + vaultTimeout: await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id), + ), vaultTimeoutAction: await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), ), pin: this.userHasPinSet, biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(), @@ -299,7 +310,9 @@ export class SettingsComponent implements OnInit { this.refreshTimeoutSettings$ .pipe( - switchMap(() => this.vaultTimeoutSettingsService.vaultTimeoutAction$()), + switchMap(() => + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), + ), takeUntil(this.destroy$), ) .subscribe((action) => { @@ -357,7 +370,7 @@ export class SettingsComponent implements OnInit { }); } - async saveVaultTimeout(newValue: number) { + async saveVaultTimeout(newValue: VaultTimeout) { if (newValue == null) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, @@ -387,7 +400,10 @@ export class SettingsComponent implements OnInit { this.previousVaultTimeout = this.form.value.vaultTimeout; + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, newValue, this.form.value.vaultTimeoutAction, ); @@ -418,7 +434,10 @@ export class SettingsComponent implements OnInit { return; } + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAccount.id, this.form.value.vaultTimeout, newValue, ); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 056fb3f51e..806fa6de1b 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -41,6 +41,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UserId } from "@bitwarden/common/types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -64,12 +65,6 @@ const BroadcasterSubscriptionId = "AppComponent"; const IdleTimeout = 60000 * 10; // 10 minutes const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours -const systemTimeoutOptions = { - onLock: -2, - onSuspend: -3, - onIdle: -4, -}; - @Component({ selector: "app-root", styles: [], @@ -430,13 +425,13 @@ export class AppComponent implements OnInit, OnDestroy { break; } case "systemSuspended": - await this.checkForSystemTimeout(systemTimeoutOptions.onSuspend); + await this.checkForSystemTimeout(VaultTimeoutStringType.OnSleep); break; case "systemLocked": - await this.checkForSystemTimeout(systemTimeoutOptions.onLock); + await this.checkForSystemTimeout(VaultTimeoutStringType.OnLocked); break; case "systemIdle": - await this.checkForSystemTimeout(systemTimeoutOptions.onIdle); + await this.checkForSystemTimeout(VaultTimeoutStringType.OnIdle); break; case "openLoginApproval": if (message.notificationId != null) { @@ -721,7 +716,7 @@ export class AppComponent implements OnInit, OnDestroy { } } - private async checkForSystemTimeout(timeout: number): Promise { + private async checkForSystemTimeout(timeout: VaultTimeout): Promise { const accounts = await firstValueFrom(this.accountService.accounts$); for (const userId in accounts) { if (userId == null) { @@ -738,9 +733,13 @@ export class AppComponent implements OnInit, OnDestroy { } } - private async getVaultTimeoutOptions(userId: string): Promise<[number, string]> { - const timeout = await this.stateService.getVaultTimeout({ userId: userId }); - const action = await this.stateService.getVaultTimeoutAction({ userId: userId }); + private async getVaultTimeoutOptions(userId: string): Promise<[VaultTimeout, string]> { + const timeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + const action = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); return [timeout, action]; } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index a5f62804aa..8d80097053 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -14,6 +14,7 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, STATE_FACTORY, + DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; @@ -56,6 +57,7 @@ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/s // 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService } from "@bitwarden/components"; @@ -138,6 +140,10 @@ const safeProviders: SafeProvider[] = [ provide: SUPPORTS_SECURE_STORAGE, useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, }), + safeProvider({ + provide: DEFAULT_VAULT_TIMEOUT, + useValue: VaultTimeoutStringType.OnRestart, + }), safeProvider({ provide: I18nServiceAbstraction, useClass: I18nRendererService, diff --git a/apps/desktop/src/models/account.ts b/apps/desktop/src/models/account.ts index 0291fdeb28..b3d3128413 100644 --- a/apps/desktop/src/models/account.ts +++ b/apps/desktop/src/models/account.ts @@ -4,7 +4,6 @@ import { } from "@bitwarden/common/platform/models/domain/account"; export class AccountSettings extends BaseAccountSettings { - vaultTimeout = -1; // On Restart dismissedBiometricRequirePasswordOnStartCallout?: boolean; } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index c60280014c..a7578d0ae2 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -13,6 +13,7 @@ import { OBSERVABLE_DISK_LOCAL_STORAGE, WINDOW, SafeInjectionToken, + DEFAULT_VAULT_TIMEOUT, CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; @@ -41,6 +42,7 @@ import { DefaultThemeStateService, ThemeStateService, } from "@bitwarden/common/platform/theming/theme-state.service"; +import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { PolicyListService } from "../admin-console/core/policy-list.service"; import { HtmlStorageService } from "../core/html-storage.service"; @@ -69,6 +71,12 @@ const safeProviders: SafeProvider[] = [ safeProvider(RouterService), safeProvider(EventService), safeProvider(PolicyListService), + safeProvider({ + provide: DEFAULT_VAULT_TIMEOUT, + deps: [PlatformUtilsServiceAbstraction], + useFactory: (platformUtilsService: PlatformUtilsServiceAbstraction): VaultTimeout => + platformUtilsService.isDev() ? VaultTimeoutStringType.Never : 15, + }), safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => void>, useFactory: (initService: InitService) => initService.init(), diff --git a/apps/web/src/app/core/state/account.ts b/apps/web/src/app/core/state/account.ts index 0cb16505e3..b6beafe31f 100644 --- a/apps/web/src/app/core/state/account.ts +++ b/apps/web/src/app/core/state/account.ts @@ -1,20 +1,8 @@ -import { - Account as BaseAccount, - AccountSettings as BaseAccountSettings, -} from "@bitwarden/common/platform/models/domain/account"; - -export class AccountSettings extends BaseAccountSettings { - vaultTimeout: number = process.env.NODE_ENV === "development" ? null : 15; -} +import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account"; +// TODO: platform to clean up accounts in later PR export class Account extends BaseAccount { - settings?: AccountSettings = new AccountSettings(); - constructor(init: Partial) { super(init); - Object.assign(this.settings, { - ...new AccountSettings(), - ...this.settings, - }); } } diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index 7f9eabb6b3..a6443b453e 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -5,6 +5,7 @@ import { concatMap, filter, firstValueFrom, map, Observable, Subject, takeUntil, import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,6 +13,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { + VaultTimeout, + VaultTimeoutOption, + VaultTimeoutStringType, +} from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; @Component({ @@ -28,7 +34,7 @@ export class PreferencesComponent implements OnInit { timeout: { hours: number; minutes: number }; action: VaultTimeoutAction; }>; - vaultTimeoutOptions: { name: string; value: number }[]; + vaultTimeoutOptions: VaultTimeoutOption[]; localeOptions: any[]; themeOptions: any[]; @@ -36,7 +42,7 @@ export class PreferencesComponent implements OnInit { private destroy$ = new Subject(); form = this.formBuilder.group({ - vaultTimeout: [null as number | null], + vaultTimeout: [null as VaultTimeout | null], vaultTimeoutAction: [VaultTimeoutAction.Lock], enableFavicons: true, theme: [ThemeType.Light], @@ -52,6 +58,7 @@ export class PreferencesComponent implements OnInit { private themeStateService: ThemeStateService, private domainSettingsService: DomainSettingsService, private dialogService: DialogService, + private accountService: AccountService, ) { this.vaultTimeoutOptions = [ { name: i18nService.t("oneMinute"), value: 1 }, @@ -60,10 +67,13 @@ export class PreferencesComponent implements OnInit { { name: i18nService.t("thirtyMinutes"), value: 30 }, { name: i18nService.t("oneHour"), value: 60 }, { name: i18nService.t("fourHours"), value: 240 }, - { name: i18nService.t("onRefresh"), value: -1 }, + { name: i18nService.t("onRefresh"), value: VaultTimeoutStringType.OnRestart }, ]; if (this.platformUtilsService.isDev()) { - this.vaultTimeoutOptions.push({ name: i18nService.t("never"), value: null }); + this.vaultTimeoutOptions.push({ + name: i18nService.t("never"), + value: VaultTimeoutStringType.Never, + }); } const localeOptions: any[] = []; @@ -130,10 +140,15 @@ export class PreferencesComponent implements OnInit { takeUntil(this.destroy$), ) .subscribe(); + + const activeAcct = await firstValueFrom(this.accountService.activeAccount$); + const initialFormValues = { - vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), + vaultTimeout: await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAcct.id), + ), vaultTimeoutAction: await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAcct.id), ), enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$), theme: await firstValueFrom(this.themeStateService.selectedTheme$), @@ -154,7 +169,10 @@ export class PreferencesComponent implements OnInit { } const values = this.form.value; + const activeAcct = await firstValueFrom(this.accountService.activeAccount$); + await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( + activeAcct.id, values.vaultTimeout, values.vaultTimeoutAction, ); diff --git a/libs/angular/src/components/settings/vault-timeout-input.component.ts b/libs/angular/src/components/settings/vault-timeout-input.component.ts index 0b0fb13d0f..f5afd181f2 100644 --- a/libs/angular/src/components/settings/vault-timeout-input.component.ts +++ b/libs/angular/src/components/settings/vault-timeout-input.component.ts @@ -14,9 +14,10 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { VaultTimeout, VaultTimeoutOption } from "@bitwarden/common/types/vault-timeout.type"; interface VaultTimeoutFormValue { - vaultTimeout: number | null; + vaultTimeout: VaultTimeout | null; custom: { hours: number | null; minutes: number | null; @@ -48,14 +49,14 @@ export class VaultTimeoutInputComponent }), }); - @Input() vaultTimeoutOptions: { name: string; value: number }[]; + @Input() vaultTimeoutOptions: VaultTimeoutOption[]; vaultTimeoutPolicy: Policy; vaultTimeoutPolicyHours: number; vaultTimeoutPolicyMinutes: number; protected canLockVault$: Observable; - private onChange: (vaultTimeout: number) => void; + private onChange: (vaultTimeout: VaultTimeout) => void; private validatorChange: () => void; private destroy$ = new Subject(); @@ -198,12 +199,24 @@ export class VaultTimeoutInputComponent this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60); this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60; - this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter( - (t) => - t.value <= this.vaultTimeoutPolicy.data.minutes && - (t.value > 0 || t.value === VaultTimeoutInputComponent.CUSTOM_VALUE) && - t.value != null, - ); - this.validatorChange(); + this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter((vaultTimeoutOption) => { + // Always include the custom option + if (vaultTimeoutOption.value === VaultTimeoutInputComponent.CUSTOM_VALUE) { + return true; + } + + if (typeof vaultTimeoutOption.value === "number") { + // Include numeric values that are less than or equal to the policy minutes + return vaultTimeoutOption.value <= this.vaultTimeoutPolicy.data.minutes; + } + + // Exclude all string cases when there's a numeric policy defined + return false; + }); + + // Only call validator change if it's been set + if (this.validatorChange) { + this.validatorChange(); + } } } diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index c58931ce55..9a94659e69 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -9,6 +9,7 @@ import { import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message } from "@bitwarden/common/platform/messaging"; +import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; declare const tag: unique symbol; /** @@ -47,6 +48,7 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken("LOG_MAC_FAILURE export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken>( "SYSTEM_THEME_OBSERVABLE", ); +export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken("DEFAULT_VAULT_TIMEOUT"); export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken>>( "INTRAPROCESS_MESSAGING_SUBJECT", ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3d68e1240b..ee14d97e68 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -274,6 +274,7 @@ import { SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, WINDOW, + DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, } from "./injection-tokens"; @@ -392,6 +393,7 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, BillingAccountProfileStateService, + VaultTimeoutSettingsServiceAbstraction, KdfConfigServiceAbstraction, ], }), @@ -573,7 +575,7 @@ const safeProviders: SafeProvider[] = [ PlatformUtilsServiceAbstraction, EnvironmentService, AppIdServiceAbstraction, - StateServiceAbstraction, + VaultTimeoutSettingsServiceAbstraction, LOGOUT_CALLBACK, ], }), @@ -646,8 +648,10 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, TokenServiceAbstraction, PolicyServiceAbstraction, - StateServiceAbstraction, BiometricStateService, + StateProvider, + LogService, + DEFAULT_VAULT_TIMEOUT, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 76c0444b5a..d2aac323bf 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; @@ -8,6 +9,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -16,6 +18,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; @@ -45,6 +48,7 @@ describe("AuthRequestLoginStrategy", () => { let userDecryptionOptions: MockProxy; let deviceTrustService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; @@ -79,6 +83,7 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions = mock(); deviceTrustService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); accountService = mockAccountServiceWith(mockUserId); @@ -106,11 +111,27 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions, deviceTrustService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); tokenResponse = identityTokenResponseFactory(); apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => { diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 43c9af35cd..54654e1d82 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -2,6 +2,7 @@ import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -64,6 +65,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -80,6 +82,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 0c2205262c..627c852076 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -114,6 +116,7 @@ describe("LoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; @@ -139,6 +142,8 @@ describe("LoginStrategy", () => { passwordStrengthService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); + appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.calledWith(accessToken).mockResolvedValue(decodedToken); @@ -161,6 +166,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); @@ -179,6 +185,21 @@ describe("LoginStrategy", () => { masterKey = new SymmetricCryptoKey( new Uint8Array(masterKeyBytesLength).buffer as CsprngArray, ) as MasterKey; + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sets the local environment after a successful login with master password", async () => { @@ -186,10 +207,19 @@ describe("LoginStrategy", () => { apiService.postIdentityToken.mockResolvedValue(idTokenResponse); const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + const mockVaultTimeout = 1000; - stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); - stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); await passwordLoginStrategy.logIn(credentials); @@ -223,10 +253,20 @@ describe("LoginStrategy", () => { apiService.postIdentityToken.mockResolvedValue(idTokenResponse); const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + const mockVaultTimeout = 1000; - stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); - stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); accountService.switchAccount = jest.fn(); // block internal switch to new account accountService.activeAccountSubject.next(null); // simulate no active account @@ -297,6 +337,22 @@ describe("LoginStrategy", () => { }); describe("Two-factor authentication", () => { + beforeEach(() => { + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); + }); + it("rejects login if 2FA is required", async () => { // Sample response where TOTP 2FA required const tokenResponse = new IdentityTwoFactorResponse({ @@ -421,6 +477,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index adcf753325..2065f898be 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,7 @@ import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -75,6 +76,7 @@ export abstract class LoginStrategy { protected twoFactorService: TwoFactorService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected KdfConfigService: KdfConfigService, ) {} @@ -163,27 +165,14 @@ export abstract class LoginStrategy { */ protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); - const userId = accountInformation.sub as UserId; - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); - const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); - await this.accountService.addAccount(userId, { name: accountInformation.name, email: accountInformation.email, emailVerified: accountInformation.email_verified, }); - // set access token and refresh token before account initialization so authN status can be accurate - // User id will be derived from the access token. - await this.tokenService.setTokens( - tokenResponse.accessToken, - vaultTimeoutAction as VaultTimeoutAction, - vaultTimeout, - tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. - ); - await this.accountService.switchAccount(userId); await this.stateService.addAccount( @@ -201,10 +190,27 @@ export abstract class LoginStrategy { await this.verifyAccountAdded(userId); + // We must set user decryption options before retrieving vault timeout settings + // as the user decryption options help determine the available timeout actions. await this.userDecryptionOptionsService.setUserDecryptionOptions( UserDecryptionOptions.fromResponse(tokenResponse), ); + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + + // User id will be derived from the access token. + await this.tokenService.setTokens( + tokenResponse.accessToken, + vaultTimeoutAction as VaultTimeoutAction, + vaultTimeout, + tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. + ); + await this.KdfConfigService.setKdfConfig( userId as UserId, tokenResponse.kdf === KdfType.PBKDF2_SHA256 diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 5c0ec93771..b6d1e07a26 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -12,6 +13,7 @@ import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/respons import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -21,6 +23,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, @@ -72,6 +75,7 @@ describe("PasswordLoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; @@ -96,6 +100,7 @@ describe("PasswordLoginStrategy", () => { policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); @@ -132,12 +137,28 @@ describe("PasswordLoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); tokenResponse = identityTokenResponseFactory(masterPasswordPolicy); apiService.postIdentityToken.mockResolvedValue(tokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sends master password credentials to the server", async () => { diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index f78897d99f..b855e25e1d 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -2,6 +2,7 @@ import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -90,6 +91,7 @@ export class PasswordLoginStrategy extends LoginStrategy { private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -106,6 +108,7 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index f821ce9a6b..b6290742be 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; @@ -12,6 +13,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,6 +24,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; @@ -55,6 +58,7 @@ describe("SsoLoginStrategy", () => { let authRequestService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let ssoLoginStrategy: SsoLoginStrategy; @@ -88,6 +92,7 @@ describe("SsoLoginStrategy", () => { authRequestService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -96,6 +101,21 @@ describe("SsoLoginStrategy", () => { sub: userId, }); + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); + ssoLoginStrategy = new SsoLoginStrategy( null, accountService, @@ -115,6 +135,7 @@ describe("SsoLoginStrategy", () => { authRequestService, i18nService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index c73252a1de..414af4c1a3 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -2,6 +2,7 @@ import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; @@ -100,6 +101,7 @@ export class SsoLoginStrategy extends LoginStrategy { private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -116,6 +118,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 820af8613e..8120a5ad39 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -21,6 +21,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; @@ -50,11 +51,15 @@ describe("UserApiLoginStrategy", () => { let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeout = 1000; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; @@ -78,6 +83,7 @@ describe("UserApiLoginStrategy", () => { keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); @@ -103,10 +109,23 @@ describe("UserApiLoginStrategy", () => { environmentService, keyConnectorService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); + + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("sends api key credentials to the server", async () => { @@ -131,11 +150,6 @@ describe("UserApiLoginStrategy", () => { it("sets the local environment after a successful login", async () => { apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); - const mockVaultTimeoutAction = VaultTimeoutAction.Lock; - const mockVaultTimeout = 60; - stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); - stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); - await apiLogInStrategy.logIn(credentials); expect(tokenService.setClientId).toHaveBeenCalledWith( diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 5e4124492d..86113d3655 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -2,6 +2,7 @@ import { firstValueFrom, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; @@ -58,6 +59,7 @@ export class UserApiLoginStrategy extends LoginStrategy { private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, ) { super( @@ -74,6 +76,7 @@ export class UserApiLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); this.cache = new BehaviorSubject(data); @@ -130,8 +133,12 @@ export class UserApiLoginStrategy extends LoginStrategy { protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const userId = await super.saveAccountInformation(tokenResponse); - const vaultTimeout = await this.stateService.getVaultTimeout(); - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); const tokenRequest = this.cache.value.tokenRequest; diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index afac2c2e6a..0db41c1e64 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; @@ -10,6 +11,7 @@ import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/mod import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -18,6 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; @@ -44,6 +47,7 @@ describe("WebAuthnLoginStrategy", () => { let twoFactorService!: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; @@ -85,6 +89,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService = mock(); userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -108,6 +113,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); @@ -116,6 +122,22 @@ describe("WebAuthnLoginStrategy", () => { const deviceResponse = new WebAuthnLoginAssertionResponseRequest(publicKeyCredential); const prfKey = new SymmetricCryptoKey(randomBytes(32)) as PrfKey; webAuthnCredentials = new WebAuthnLoginCredentials(token, deviceResponse, prfKey); + + // Mock vault timeout settings + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); afterAll(() => { diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 4b5441d00a..226ab1799a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -2,6 +2,7 @@ import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -58,6 +59,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + vaultTimeoutSettingsService: VaultTimeoutSettingsService, kdfConfigService: KdfConfigService, ) { super( @@ -74,6 +76,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index f1b5590404..f0a8d81bea 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; @@ -14,6 +16,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -67,6 +70,7 @@ describe("LoginStrategyService", () => { let authRequestService: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; let kdfConfigService: MockProxy; let stateProvider: FakeGlobalStateProvider; @@ -97,6 +101,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); stateProvider = new FakeGlobalStateProvider(); + vaultTimeoutSettingsService = mock(); kdfConfigService = mock(); sut = new LoginStrategyService( @@ -122,10 +127,26 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, + vaultTimeoutSettingsService, kdfConfigService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeoutActionBSub = new BehaviorSubject( + mockVaultTimeoutAction, + ); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + mockVaultTimeoutActionBSub.asObservable(), + ); + + const mockVaultTimeout = 1000; + + const mockVaultTimeoutBSub = new BehaviorSubject(mockVaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue( + mockVaultTimeoutBSub.asObservable(), + ); }); it("should return an AuthResult on successful login", async () => { diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 13cca69b3a..46d785f9b5 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -8,6 +8,7 @@ import { } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; @@ -110,6 +111,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected vaultTimeoutSettingsService: VaultTimeoutSettingsService, protected kdfConfigService: KdfConfigService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); @@ -361,6 +363,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.policyService, this, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.Sso: @@ -383,6 +386,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestService, this.i18nService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.UserApiKey: @@ -403,6 +407,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.AuthRequest: @@ -422,6 +427,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.userDecryptionOptionsService, this.deviceTrustService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); case AuthenticationType.WebAuthn: @@ -440,6 +446,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, this.kdfConfigService, ); } diff --git a/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts index 03ae320e54..5bf38f3b57 100644 --- a/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/abstractions/vault-timeout/vault-timeout-settings.service.ts @@ -1,16 +1,19 @@ import { Observable } from "rxjs"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserId } from "../../types/guid"; +import { VaultTimeout } from "../../types/vault-timeout.type"; export abstract class VaultTimeoutSettingsService { /** * Set the vault timeout options for the user * @param vaultTimeout The vault timeout in minutes * @param vaultTimeoutAction The vault timeout action - * @param userId The user id to set. If not provided, the current user is used + * @param userId The user id to set the data for. */ setVaultTimeoutOptions: ( - vaultTimeout: number, + userId: UserId, + vaultTimeout: VaultTimeout, vaultTimeoutAction: VaultTimeoutAction, ) => Promise; @@ -23,19 +26,23 @@ export abstract class VaultTimeoutSettingsService { availableVaultTimeoutActions$: (userId?: string) => Observable; /** - * Get the current vault timeout action for the user. This is not the same as the current state, it is - * calculated based on the current state, the user's policy, and the user's available unlock methods. + * Gets the vault timeout action for the given user id. The returned value is + * calculated based on the current state, if a max vault timeout policy applies to the user, + * and what the user's available unlock methods are. + * + * A new action will be emitted if the current state changes or if the user's policy changes and the new policy affects the action. + * @param userId - the user id to get the vault timeout action for */ - getVaultTimeout: (userId?: string) => Promise; + getVaultTimeoutActionByUserId$: (userId: string) => Observable; /** - * Observe the vault timeout action for the user. This is calculated based on users preferred lock action saved in the state, - * the user's policy, and the user's available unlock methods. + * Get the vault timeout for the given user id. The returned value is calculated based on the current state + * and if a max vault timeout policy applies to the user. * - * **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes - * @param userId The user id to check. If not provided, the current user is used + * A new timeout will be emitted if the current state changes or if the user's policy changes and the new policy affects the timeout. + * @param userId The user id to get the vault timeout for */ - vaultTimeoutAction$: (userId?: string) => Observable; + getVaultTimeoutByUserId$: (userId: string) => Observable; /** * Has the user enabled unlock with Biometric. diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index fc3bd317f4..d078051f64 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -2,6 +2,7 @@ import { Observable } from "rxjs"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { UserId } from "../../types/guid"; +import { VaultTimeout } from "../../types/vault-timeout.type"; import { DecodedAccessToken } from "../services/token.service"; export abstract class TokenService { @@ -27,7 +28,7 @@ export abstract class TokenService { setTokens: ( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, refreshToken?: string, clientIdClientSecret?: [string, string], ) => Promise; @@ -51,7 +52,7 @@ export abstract class TokenService { setAccessToken: ( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, ) => Promise; // TODO: revisit having this public clear method approach once the state service is fully deprecated. @@ -90,7 +91,7 @@ export abstract class TokenService { setClientId: ( clientId: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ) => Promise; @@ -110,7 +111,7 @@ export abstract class TokenService { setClientSecret: ( clientSecret: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ) => Promise; diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 3e92053d2f..9c5dd9fc91 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -10,9 +10,10 @@ import { AbstractStorageService } from "../../platform/abstractions/storage.serv import { StorageLocation } from "../../platform/enums"; import { StorageOptions } from "../../platform/models/domain/storage-options"; import { UserId } from "../../types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; -import { DecodedAccessToken, TokenService } from "./token.service"; +import { DecodedAccessToken, TokenService, TokenStorageLocation } from "./token.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, @@ -37,10 +38,10 @@ describe("TokenService", () => { let logService: MockProxy; const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut; - const memoryVaultTimeout = 30; + const memoryVaultTimeout: VaultTimeout = 30; const diskVaultTimeoutAction = VaultTimeoutAction.Lock; - const diskVaultTimeout: number = null; + const diskVaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; const accessTokenJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwibmJmIjoxNzA5MzI0MTExLCJpYXQiOjE3MDkzMjQxMTEsImV4cCI6MTcwOTMyNzcxMSwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl0sImNsaWVudF9pZCI6IndlYiIsInN1YiI6ImVjZTcwYTEzLTcyMTYtNDNjNC05OTc3LWIxMDMwMTQ2ZTFlNyIsImF1dGhfdGltZSI6MTcwOTMyNDEwNCwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoiZXhhbXBsZUBiaXR3YXJkZW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJzc3RhbXAiOiJHWTdKQU82NENLS1RLQkI2WkVBVVlMMldPUVU3QVNUMiIsIm5hbWUiOiJUZXN0IFVzZXIiLCJvcmdvd25lciI6WyI5MmI0OTkwOC1iNTE0LTQ1YTgtYmFkYi1iMTAzMDE0OGZlNTMiLCIzOGVkZTMyMi1iNGI0LTRiZDgtOWUwOS1iMTA3MDExMmRjMTEiLCJiMmQwNzAyOC1hNTgzLTRjM2UtOGQ2MC1iMTA3MDExOThjMjkiLCJiZjkzNGJhMi0wZmQ0LTQ5ZjItYTk1ZS1iMTA3MDExZmM5ZTYiLCJjMGI3Zjc1ZC0wMTVmLTQyYzktYjNhNi1iMTA4MDE3NjA3Y2EiXSwiZGV2aWNlIjoiNGI4NzIzNjctMGRhNi00MWEwLWFkY2ItNzdmMmZlZWZjNGY0IiwianRpIjoiNzUxNjFCRTQxMzFGRjVBMkRFNTExQjhDNEUyRkY4OUEifQ.n7roP8sSbfwcYdvRxZNZds27IK32TW6anorE6BORx_Q"; @@ -163,21 +164,53 @@ describe("TokenService", () => { describe("setAccessToken", () => { it("should throw an error if the access token is null", async () => { // Act - const result = tokenService.setAccessToken(null, VaultTimeoutAction.Lock, null); + const result = tokenService.setAccessToken( + null, + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).rejects.toThrow("Access token is required."); }); it("should throw an error if an invalid token is passed in", async () => { // Act - const result = tokenService.setAccessToken("invalidToken", VaultTimeoutAction.Lock, null); + const result = tokenService.setAccessToken( + "invalidToken", + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).rejects.toThrow("JWT must have 3 parts"); }); - it("should not throw an error as long as the token is valid", async () => { + it("should throw an error if the vault timeout is missing", async () => { // Act const result = tokenService.setAccessToken(accessTokenJwt, VaultTimeoutAction.Lock, null); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Act + const result = tokenService.setAccessToken( + accessTokenJwt, + null, + VaultTimeoutStringType.Never, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + + it("should not throw an error as long as the token is valid", async () => { + // Act + const result = tokenService.setAccessToken( + accessTokenJwt, + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).resolves.not.toThrow(); }); @@ -1053,6 +1086,32 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot save refresh token."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Act + const result = (tokenService as any).setRefreshToken( + refreshToken, + VaultTimeoutAction.Lock, + null, + userIdFromAccessToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Act + const result = (tokenService as any).setRefreshToken( + refreshToken, + null, + VaultTimeoutStringType.Never, + userIdFromAccessToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + describe("Memory storage tests", () => { it("should set the refresh token in memory for the specified user id", async () => { // Act @@ -1382,6 +1441,34 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot save client id."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientId(clientId, VaultTimeoutAction.Lock, null); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientId(clientId, null, VaultTimeoutStringType.Never); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + describe("Memory storage tests", () => { it("should set the client id in memory when there is an active user in global state", async () => { // Arrange @@ -1618,11 +1705,47 @@ describe("TokenService", () => { it("should throw an error if no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error - const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null); + const result = tokenService.setClientSecret( + clientSecret, + VaultTimeoutAction.Lock, + VaultTimeoutStringType.Never, + ); // Assert await expect(result).rejects.toThrow("User id not found. Cannot save client secret."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Arrange + + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + const result = tokenService.setClientSecret( + clientSecret, + null, + VaultTimeoutStringType.Never, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + describe("Memory storage tests", () => { it("should set the client secret in memory when there is an active user in global state", async () => { // Arrange @@ -1991,6 +2114,42 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Access token is required."); }); + it("should throw an error if the vault timeout is missing", async () => { + // Arrange + const refreshToken = "refreshToken"; + const vaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = null; + + // Act + const result = tokenService.setTokens( + accessTokenJwt, + vaultTimeoutAction, + vaultTimeout, + refreshToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout is required."); + }); + + it("should throw an error if the vault timeout action is missing", async () => { + // Arrange + const refreshToken = "refreshToken"; + const vaultTimeoutAction: VaultTimeoutAction = null; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + + // Act + const result = tokenService.setTokens( + accessTokenJwt, + vaultTimeoutAction, + vaultTimeout, + refreshToken, + ); + + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action is required."); + }); + it("should not throw an error if the refresh token is missing and it should just not set it", async () => { // Arrange const refreshToken: string = null; @@ -2270,6 +2429,168 @@ describe("TokenService", () => { }); }); + describe("determineStorageLocation", () => { + it("should throw an error if the vault timeout is null", async () => { + // Arrange + const vaultTimeoutAction: VaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = null; + // Act + const result = (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + false, + ); + // Assert + await expect(result).rejects.toThrow( + "TokenService - determineStorageLocation: We expect the vault timeout to always exist at this point.", + ); + }); + + it("should throw an error if the vault timeout action is null", async () => { + // Arrange + const vaultTimeoutAction: VaultTimeoutAction = null; + const vaultTimeout: VaultTimeout = 0; + // Act + const result = (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + false, + ); + // Assert + await expect(result).rejects.toThrow( + "TokenService - determineStorageLocation: We expect the vault timeout action to always exist at this point.", + ); + }); + + describe("Secure storage disabled", () => { + beforeEach(() => { + const supportsSecureStorage = false; + tokenService = createTokenService(supportsSecureStorage); + }); + + it.each([ + [VaultTimeoutStringType.OnRestart], + [VaultTimeoutStringType.OnLocked], + [VaultTimeoutStringType.OnSleep], + [VaultTimeoutStringType.OnIdle], + [0], + [30], + [60], + [90], + [120], + ])( + "returns memory when the vault timeout action is logout and the vault timeout is defined %s (not Never)", + async (vaultTimeout: VaultTimeout) => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const useSecureStorage = false; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Memory); + }, + ); + + it("returns disk when the vault timeout action is logout and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = false; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Disk); + }); + + it("returns disk when the vault timeout action is lock and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = false; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Disk); + }); + }); + + describe("Secure storage enabled", () => { + beforeEach(() => { + const supportsSecureStorage = true; + tokenService = createTokenService(supportsSecureStorage); + }); + + it.each([ + [VaultTimeoutStringType.OnRestart], + [VaultTimeoutStringType.OnLocked], + [VaultTimeoutStringType.OnSleep], + [VaultTimeoutStringType.OnIdle], + [0], + [30], + [60], + [90], + [120], + ])( + "returns memory when the vault timeout action is logout and the vault timeout is defined %s (not Never)", + async (vaultTimeout: VaultTimeout) => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const useSecureStorage = true; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.Memory); + }, + ); + + it("returns secure storage when the vault timeout action is logout and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.LogOut; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = true; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.SecureStorage); + }); + + it("returns secure storage when the vault timeout action is lock and the vault timeout is never", async () => { + // Arrange + const vaultTimeoutAction = VaultTimeoutAction.Lock; + const vaultTimeout: VaultTimeout = VaultTimeoutStringType.Never; + const useSecureStorage = true; + // Act + const result = await (tokenService as any).determineStorageLocation( + vaultTimeoutAction, + vaultTimeout, + useSecureStorage, + ); + // Assert + expect(result).toEqual(TokenStorageLocation.SecureStorage); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 56311671ad..203d95429e 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -19,6 +19,7 @@ import { UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; @@ -159,7 +160,7 @@ export class TokenService implements TokenServiceAbstraction { async setTokens( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, refreshToken?: string, clientIdClientSecret?: [string, string], ): Promise { @@ -167,6 +168,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("Access token is required."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + // get user id the access token const userId: UserId = await this.getUserIdFromAccessToken(accessToken); @@ -272,7 +282,7 @@ export class TokenService implements TokenServiceAbstraction { private async _setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId: UserId, ): Promise { const storageLocation = await this.determineStorageLocation( @@ -319,7 +329,7 @@ export class TokenService implements TokenServiceAbstraction { async setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, ): Promise { if (!accessToken) { throw new Error("Access token is required."); @@ -331,6 +341,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save access token."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + await this._setAccessToken(accessToken, vaultTimeoutAction, vaultTimeout, userId); } @@ -413,7 +432,7 @@ export class TokenService implements TokenServiceAbstraction { private async setRefreshToken( refreshToken: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId: UserId, ): Promise { // If we don't have a user id, we can't save the value @@ -421,6 +440,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save refresh token."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, @@ -521,7 +549,7 @@ export class TokenService implements TokenServiceAbstraction { async setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); @@ -531,6 +559,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save client id."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, @@ -589,7 +626,7 @@ export class TokenService implements TokenServiceAbstraction { async setClientSecret( clientSecret: string, vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, userId?: UserId, ): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); @@ -598,6 +635,15 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot save client secret."); } + // Can't check for falsey b/c 0 is a valid value + if (vaultTimeout == null) { + throw new Error("Vault Timeout is required."); + } + + if (vaultTimeoutAction == null) { + throw new Error("Vault Timeout Action is required."); + } + const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, @@ -885,10 +931,25 @@ export class TokenService implements TokenServiceAbstraction { private async determineStorageLocation( vaultTimeoutAction: VaultTimeoutAction, - vaultTimeout: number | null, + vaultTimeout: VaultTimeout, useSecureStorage: boolean, ): Promise { - if (vaultTimeoutAction === VaultTimeoutAction.LogOut && vaultTimeout != null) { + if (vaultTimeoutAction == null) { + throw new Error( + "TokenService - determineStorageLocation: We expect the vault timeout action to always exist at this point.", + ); + } + + if (vaultTimeout == null) { + throw new Error( + "TokenService - determineStorageLocation: We expect the vault timeout to always exist at this point.", + ); + } + + if ( + vaultTimeoutAction === VaultTimeoutAction.LogOut && + vaultTimeout !== VaultTimeoutStringType.Never + ) { return TokenStorageLocation.Memory; } else { if (useSecureStorage && this.platformSupportsSecureStorage) { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1073ea232..0f678a6bf3 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -118,8 +118,4 @@ export abstract class StateService { getGeneratorOptions: (options?: StorageOptions) => Promise; setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; - getVaultTimeout: (options?: StorageOptions) => Promise; - setVaultTimeout: (value: number, options?: StorageOptions) => Promise; - getVaultTimeoutAction: (options?: StorageOptions) => Promise; - setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; } diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 892d608931..8c7d70948c 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -147,8 +147,6 @@ export class AccountSettings { passwordGenerationOptions?: PasswordGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions; generatorOptions?: GeneratorOptions; - vaultTimeout?: number; - vaultTimeoutAction?: string = "lock"; static fromJSON(obj: Jsonify): AccountSettings { if (obj == null) { diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index cd7cf7d174..2d48f09e92 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -1,7 +1,5 @@ export class GlobalState { organizationInvitation?: any; - vaultTimeout?: number; - vaultTimeoutAction?: string; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; enableDuckDuckGoBrowserIntegration?: boolean; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 5bf8b57e0f..8bb1289419 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -7,9 +7,11 @@ import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-sta import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; +import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { UserKey, MasterKey } from "../../types/key"; +import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { KeyGenerationService } from "../abstractions/key-generation.service"; @@ -220,8 +222,8 @@ describe("cryptoService", () => { }); describe("Auto Key refresh", () => { - it("sets an Auto key if vault timeout is set to null", async () => { - stateService.getVaultTimeout.mockResolvedValue(null); + it("sets an Auto key if vault timeout is set to 'never'", async () => { + await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId); @@ -231,7 +233,7 @@ describe("cryptoService", () => { }); it("clears the Auto key if vault timeout is set to anything other than null", async () => { - stateService.getVaultTimeout.mockResolvedValue(10); + await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 2ac8b0f0f1..fed22e06a0 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -11,6 +11,7 @@ import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; +import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state"; import { CsprngArray } from "../../types/csprng"; import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { @@ -22,6 +23,7 @@ import { UserPrivateKey, UserPublicKey, } from "../../types/key"; +import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service"; import { EncryptService } from "../abstractions/encrypt.service"; @@ -773,8 +775,14 @@ export class CryptoService implements CryptoServiceAbstraction { let shouldStoreKey = false; switch (keySuffix) { case KeySuffixOptions.Auto: { - const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId }); - shouldStoreKey = vaultTimeout == null; + // TODO: Sharing the UserKeyDefinition is temporary to get around a circ dep issue between + // the VaultTimeoutSettingsSvc and this service. + // This should be fixed as part of the PM-7082 - Auto Key Service work. + const vaultTimeout = await firstValueFrom( + this.stateProvider.getUserState$(VAULT_TIMEOUT, userId), + ); + + shouldStoreKey = vaultTimeout == VaultTimeoutStringType.Never; break; } case KeySuffixOptions.Pin: { diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 179286a097..497e6e6703 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -571,49 +571,6 @@ export class StateService< )?.profile?.userId; } - async getVaultTimeout(options?: StorageOptions): Promise { - const accountVaultTimeout = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.vaultTimeout; - return accountVaultTimeout; - } - - async setVaultTimeout(value: number, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.vaultTimeout = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getVaultTimeoutAction(options?: StorageOptions): Promise { - const accountVaultTimeoutAction = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.vaultTimeoutAction; - return ( - accountVaultTimeoutAction ?? - ( - await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ) - )?.vaultTimeoutAction - ); - } - - async setVaultTimeoutAction(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.vaultTimeoutAction = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - protected async getGlobals(options: StorageOptions): Promise { let globals: TGlobalState; if (this.useMemory(options.storageLocation)) { diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index 2047001e36..b25898ab7c 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -73,23 +73,25 @@ export class SystemService implements SystemServiceAbstraction { clearInterval(this.reloadInterval); this.reloadInterval = null; - const currentUser = await firstValueFrom( + const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe( map((a) => a?.id), timeout(500), ), ); // Replace current active user if they will be logged out on reload - if (currentUser != null) { + if (activeUserId != null) { const timeoutAction = await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$().pipe(timeout(500)), + this.vaultTimeoutSettingsService + .getVaultTimeoutActionByUserId$(activeUserId) + .pipe(timeout(500)), // safety feature to avoid this call hanging and stopping process reload from clearing memory ); if (timeoutAction === VaultTimeoutAction.LogOut) { const nextUser = await firstValueFrom( this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), ); // Can be removed once we migrate password generation history to state providers - await this.stateService.clearDecryptedData(currentUser); + await this.stateService.clearDecryptedData(activeUserId); await this.accountService.switchAccount(nextUser); } } diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index b288b8e19d..6f225f6c2f 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -63,6 +63,13 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); +export const VAULT_TIMEOUT_SETTINGS_DISK_LOCAL = new StateDefinition( + "vaultTimeoutSettings", + "disk", + { + web: "disk-local", + }, +); // Autofill diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 84fa7bd077..4620a2ccde 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service"; +import { VaultTimeoutSettingsService } from "../abstractions/vault-timeout/vault-timeout-settings.service"; import { OrganizationConnectionType } from "../admin-console/enums"; import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request"; import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/request/organization/organization-sponsorship-redeem.request"; @@ -116,7 +117,6 @@ import { UserKeyResponse } from "../models/response/user-key.response"; import { AppIdService } from "../platform/abstractions/app-id.service"; import { EnvironmentService } from "../platform/abstractions/environment.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; -import { StateService } from "../platform/abstractions/state.service"; import { Utils } from "../platform/misc/utils"; import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; @@ -156,7 +156,7 @@ export class ApiService implements ApiServiceAbstraction { private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService, private appIdService: AppIdService, - private stateService: StateService, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private logoutCallback: (expired: boolean) => Promise, private customUserAgent: string = null, ) { @@ -1750,8 +1750,17 @@ export class ApiService implements ApiServiceAbstraction { const responseJson = await response.json(); const tokenResponse = new IdentityTokenResponse(responseJson); - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - const vaultTimeout = await this.stateService.getVaultTimeout(); + const newDecodedAccessToken = await this.tokenService.decodeAccessToken( + tokenResponse.accessToken, + ); + const userId = newDecodedAccessToken.sub; + + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); await this.tokenService.setTokens( tokenResponse.accessToken, @@ -1783,8 +1792,15 @@ export class ApiService implements ApiServiceAbstraction { throw new Error("Invalid response received when refreshing api token"); } - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - const vaultTimeout = await this.stateService.getVaultTimeout(); + const newDecodedAccessToken = await this.tokenService.decodeAccessToken(response.accessToken); + const userId = newDecodedAccessToken.sub; + + const vaultTimeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), + ); + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); await this.tokenService.setAccessToken( response.accessToken, diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 776fa46e06..894d550bda 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -9,14 +9,20 @@ import { import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; -import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; +import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { Policy } from "../../admin-console/models/domain/policy"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; -import { StateService } from "../../platform/abstractions/state.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; +import { + VAULT_TIMEOUT, + VAULT_TIMEOUT_ACTION, +} from "../../services/vault-timeout/vault-timeout-settings.state"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; @@ -27,13 +33,14 @@ describe("VaultTimeoutSettingsService", () => { let cryptoService: MockProxy; let tokenService: MockProxy; let policyService: MockProxy; - let stateService: MockProxy; const biometricStateService = mock(); - let service: VaultTimeoutSettingsService; + let vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; let userDecryptionOptionsSubject: BehaviorSubject; const mockUserId = Utils.newGuid() as UserId; + let stateProvider: FakeStateProvider; + let logService: MockProxy; beforeEach(() => { accountService = mockAccountServiceWith(mockUserId); @@ -42,7 +49,6 @@ describe("VaultTimeoutSettingsService", () => { cryptoService = mock(); tokenService = mock(); policyService = mock(); - stateService = mock(); userDecryptionOptionsSubject = new BehaviorSubject(null); userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; @@ -53,16 +59,13 @@ describe("VaultTimeoutSettingsService", () => { userDecryptionOptionsSubject, ); - service = new VaultTimeoutSettingsService( - accountService, - pinService, - userDecryptionOptionsService, - cryptoService, - tokenService, - policyService, - stateService, - biometricStateService, - ); + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + logService = mock(); + + const defaultVaultTimeout: VaultTimeout = 15; // default web vault timeout + vaultTimeoutSettingsService = createVaultTimeoutSettingsService(defaultVaultTimeout); biometricStateService.biometricUnlockEnabled$ = of(false); }); @@ -73,7 +76,9 @@ describe("VaultTimeoutSettingsService", () => { describe("availableVaultTimeoutActions$", () => { it("always returns LogOut", async () => { - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.LogOut); }); @@ -81,7 +86,9 @@ describe("VaultTimeoutSettingsService", () => { it("contains Lock when the user has a master password", async () => { userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.Lock); }); @@ -89,7 +96,9 @@ describe("VaultTimeoutSettingsService", () => { it("contains Lock when the user has either a persistent or ephemeral PIN configured", async () => { pinService.isPinSet.mockResolvedValue(true); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.Lock); }); @@ -98,7 +107,9 @@ describe("VaultTimeoutSettingsService", () => { biometricStateService.biometricUnlockEnabled$ = of(true); biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).toContain(VaultTimeoutAction.Lock); }); @@ -108,13 +119,21 @@ describe("VaultTimeoutSettingsService", () => { pinService.isPinSet.mockResolvedValue(false); biometricStateService.biometricUnlockEnabled$ = of(false); - const result = await firstValueFrom(service.availableVaultTimeoutActions$()); + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); expect(result).not.toContain(VaultTimeoutAction.Lock); }); }); - describe("vaultTimeoutAction$", () => { + describe("getVaultTimeoutActionByUserId$", () => { + it("should throw an error if no user id is provided", async () => { + expect(() => vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(null)).toThrow( + "User id required. Cannot get vault timeout action.", + ); + }); + describe("given the user has a master password", () => { it.each` policy | userPreference | expected @@ -129,9 +148,12 @@ describe("VaultTimeoutSettingsService", () => { policyService.getAll$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), ); - stateService.getVaultTimeoutAction.mockResolvedValue(userPreference); - const result = await firstValueFrom(service.vaultTimeoutAction$()); + await stateProvider.setUserState(VAULT_TIMEOUT_ACTION, userPreference, mockUserId); + + const result = await firstValueFrom( + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(mockUserId), + ); expect(result).toBe(expected); }, @@ -140,19 +162,23 @@ describe("VaultTimeoutSettingsService", () => { describe("given the user does not have a master password", () => { it.each` - unlockMethod | policy | userPreference | expected - ${false} | ${null} | ${null} | ${VaultTimeoutAction.LogOut} - ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} - ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.LogOut} - ${true} | ${null} | ${null} | ${VaultTimeoutAction.LogOut} - ${true} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock} - ${true} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock} - ${true} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} + hasPinUnlock | hasBiometricUnlock | policy | userPreference | expected + ${false} | ${false} | ${null} | ${null} | ${VaultTimeoutAction.LogOut} + ${false} | ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} + ${false} | ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.LogOut} + ${false} | ${true} | ${null} | ${null} | ${VaultTimeoutAction.Lock} + ${false} | ${true} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock} + ${false} | ${true} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock} + ${false} | ${true} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${null} | ${null} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock} + ${true} | ${false} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} `( - "returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference", - async ({ unlockMethod, policy, userPreference, expected }) => { - biometricStateService.biometricUnlockEnabled$ = of(unlockMethod); - biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(unlockMethod); + "returns $expected when policy is $policy, has PIN unlock method: $hasPinUnlock or Biometric unlock method: $hasBiometricUnlock, and user preference is $userPreference", + async ({ hasPinUnlock, hasBiometricUnlock, policy, userPreference, expected }) => { + biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(hasBiometricUnlock); + pinService.isPinSet.mockResolvedValue(hasPinUnlock); userDecryptionOptionsSubject.next( new UserDecryptionOptions({ hasMasterPassword: false }), @@ -160,13 +186,160 @@ describe("VaultTimeoutSettingsService", () => { policyService.getAll$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), ); - stateService.getVaultTimeoutAction.mockResolvedValue(userPreference); - const result = await firstValueFrom(service.vaultTimeoutAction$()); + await stateProvider.setUserState(VAULT_TIMEOUT_ACTION, userPreference, mockUserId); + + const result = await firstValueFrom( + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(mockUserId), + ); expect(result).toBe(expected); }, ); }); }); + + describe("getVaultTimeoutByUserId$", () => { + it("should throw an error if no user id is provided", async () => { + expect(() => vaultTimeoutSettingsService.getVaultTimeoutByUserId$(null)).toThrow( + "User id required. Cannot get vault timeout.", + ); + }); + + it.each([ + // policy, vaultTimeout, expected + [null, null, 15], // no policy, no vault timeout, falls back to default + [30, 90, 30], // policy overrides vault timeout + [30, 15, 15], // policy doesn't override vault timeout when it's within acceptable range + [90, VaultTimeoutStringType.Never, 90], // policy overrides vault timeout when it's "never" + [null, VaultTimeoutStringType.Never, VaultTimeoutStringType.Never], // no policy, persist "never" vault timeout + [90, 0, 0], // policy doesn't override vault timeout when it's 0 (immediate) + [null, 0, 0], // no policy, persist 0 (immediate) vault timeout + [90, VaultTimeoutStringType.OnRestart, 90], // policy overrides vault timeout when it's "onRestart" + [null, VaultTimeoutStringType.OnRestart, VaultTimeoutStringType.OnRestart], // no policy, persist "onRestart" vault timeout + [90, VaultTimeoutStringType.OnLocked, 90], // policy overrides vault timeout when it's "onLocked" + [null, VaultTimeoutStringType.OnLocked, VaultTimeoutStringType.OnLocked], // no policy, persist "onLocked" vault timeout + [90, VaultTimeoutStringType.OnSleep, 90], // policy overrides vault timeout when it's "onSleep" + [null, VaultTimeoutStringType.OnSleep, VaultTimeoutStringType.OnSleep], // no policy, persist "onSleep" vault timeout + [90, VaultTimeoutStringType.OnIdle, 90], // policy overrides vault timeout when it's "onIdle" + [null, VaultTimeoutStringType.OnIdle, VaultTimeoutStringType.OnIdle], // no policy, persist "onIdle" vault timeout + ])( + "when policy is %s, and vault timeout is %s, returns %s", + async (policy, vaultTimeout, expected) => { + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); + policyService.getAll$.mockReturnValue( + of(policy === null ? [] : ([{ data: { minutes: policy } }] as unknown as Policy[])), + ); + + await stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, mockUserId); + + const result = await firstValueFrom( + vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId), + ); + + expect(result).toBe(expected); + }, + ); + }); + + describe("setVaultTimeoutOptions", () => { + const mockAccessToken = "mockAccessToken"; + const mockRefreshToken = "mockRefreshToken"; + const mockClientId = "mockClientId"; + const mockClientSecret = "mockClientSecret"; + + it("should throw an error if no user id is provided", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(null, null, null); + // Assert + await expect(result).rejects.toThrow("User id required. Cannot set vault timeout settings."); + }); + + it("should not throw an error if 0 is provided as the timeout", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions( + mockUserId, + 0, + VaultTimeoutAction.Lock, + ); + // Assert + await expect(result).resolves.not.toThrow(); + }); + + it("should throw an error if a null vault timeout is provided", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, null, null); + // Assert + await expect(result).rejects.toThrow("Vault Timeout cannot be null."); + }); + + it("should throw an error if a null vault timout action is provided", async () => { + // note: don't await here because we want to test the error + const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, 30, null); + // Assert + await expect(result).rejects.toThrow("Vault Timeout Action cannot be null."); + }); + + it("should set the vault timeout options for the given user", async () => { + // Arrange + tokenService.getAccessToken.mockResolvedValue(mockAccessToken); + tokenService.getRefreshToken.mockResolvedValue(mockRefreshToken); + tokenService.getClientId.mockResolvedValue(mockClientId); + tokenService.getClientSecret.mockResolvedValue(mockClientSecret); + + const action = VaultTimeoutAction.Lock; + const timeout = 30; + + // Act + await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action); + + // Assert + expect(tokenService.setTokens).toHaveBeenCalledWith( + mockAccessToken, + action, + timeout, + mockRefreshToken, + [mockClientId, mockClientSecret], + ); + + expect( + stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT_ACTION).nextMock, + ).toHaveBeenCalledWith(action); + + expect( + stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT).nextMock, + ).toHaveBeenCalledWith(timeout); + + expect(cryptoService.refreshAdditionalKeys).toHaveBeenCalled(); + }); + + it("should clear the tokens when the timeout is non-null and the action is log out", async () => { + // Arrange + const action = VaultTimeoutAction.LogOut; + const timeout = 30; + + // Act + await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action); + + // Assert + expect(tokenService.clearTokens).toHaveBeenCalled(); + }); + }); + + function createVaultTimeoutSettingsService( + defaultVaultTimeout: VaultTimeout, + ): VaultTimeoutSettingsService { + return new VaultTimeoutSettingsService( + accountService, + pinService, + userDecryptionOptionsService, + cryptoService, + tokenService, + policyService, + biometricStateService, + stateProvider, + logService, + defaultVaultTimeout, + ); + } }); diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index ca763667ac..282b86fb63 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -1,4 +1,17 @@ -import { defer, firstValueFrom } from "rxjs"; +import { + EMPTY, + Observable, + catchError, + combineLatest, + defer, + distinctUntilChanged, + firstValueFrom, + from, + map, + shareReplay, + switchMap, + tap, +} from "rxjs"; import { PinServiceAbstraction, @@ -8,13 +21,18 @@ import { import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; +import { Policy } from "../../admin-console/models/domain/policy"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; -import { StateService } from "../../platform/abstractions/state.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; +import { StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; +import { VaultTimeout } from "../../types/vault-timeout.type"; + +import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state"; export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction { constructor( @@ -24,11 +42,29 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA private cryptoService: CryptoService, private tokenService: TokenService, private policyService: PolicyService, - private stateService: StateService, private biometricStateService: BiometricStateService, + private stateProvider: StateProvider, + private logService: LogService, + private defaultVaultTimeout: VaultTimeout, ) {} - async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise { + async setVaultTimeoutOptions( + userId: UserId, + timeout: VaultTimeout, + action: VaultTimeoutAction, + ): Promise { + if (!userId) { + throw new Error("User id required. Cannot set vault timeout settings."); + } + + if (timeout == null) { + throw new Error("Vault Timeout cannot be null."); + } + + if (action == null) { + throw new Error("Vault Timeout Action cannot be null."); + } + // We swap these tokens from being on disk for lock actions, and in memory for logout actions // Get them here to set them to their new location after changing the timeout action and clearing if needed const accessToken = await this.tokenService.getAccessToken(); @@ -36,20 +72,15 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA const clientId = await this.tokenService.getClientId(); const clientSecret = await this.tokenService.getClientSecret(); - await this.stateService.setVaultTimeout(timeout); + await this.setVaultTimeout(userId, timeout); - const currentAction = await this.stateService.getVaultTimeoutAction(); - - if ( - (timeout != null || timeout === 0) && - action === VaultTimeoutAction.LogOut && - action !== currentAction - ) { + if (timeout != null && action === VaultTimeoutAction.LogOut) { // if we have a vault timeout and the action is log out, reset tokens + // as the tokens were stored on disk and now should be stored in memory await this.tokenService.clearTokens(); } - await this.stateService.setVaultTimeoutAction(action); + await this.setVaultTimeoutAction(userId, action); await this.tokenService.setTokens(accessToken, action, timeout, refreshToken, [ clientId, @@ -71,72 +102,164 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA return await biometricUnlockPromise; } - async getVaultTimeout(userId?: UserId): Promise { - const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); - const policies = await firstValueFrom( - this.policyService.getAll$(PolicyType.MaximumVaultTimeout, userId), - ); - - if (policies?.length) { - // Remove negative values, and ensure it's smaller than maximum allowed value according to policy - let timeout = Math.min(vaultTimeout, policies[0].data.minutes); - - if (vaultTimeout == null || timeout < 0) { - timeout = policies[0].data.minutes; - } - - // TODO @jlf0dev: Can we move this somwhere else? Maybe add it to the initialization process? - // ( Apparently I'm the one that reviewed the original PR that added this :) ) - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - if (vaultTimeout !== timeout) { - await this.stateService.setVaultTimeout(timeout, { userId }); - } - - return timeout; + private async setVaultTimeout(userId: UserId, timeout: VaultTimeout): Promise { + if (!userId) { + throw new Error("User id required. Cannot set vault timeout."); } - return vaultTimeout; + if (timeout == null) { + throw new Error("Vault Timeout cannot be null."); + } + + await this.stateProvider.setUserState(VAULT_TIMEOUT, timeout, userId); } - vaultTimeoutAction$(userId?: UserId) { - return defer(() => this.getVaultTimeoutAction(userId)); + getVaultTimeoutByUserId$(userId: UserId): Observable { + if (!userId) { + throw new Error("User id required. Cannot get vault timeout."); + } + + return combineLatest([ + this.stateProvider.getUserState$(VAULT_TIMEOUT, userId), + this.getMaxVaultTimeoutPolicyByUserId$(userId), + ]).pipe( + switchMap(([currentVaultTimeout, maxVaultTimeoutPolicy]) => { + return from(this.determineVaultTimeout(currentVaultTimeout, maxVaultTimeoutPolicy)).pipe( + tap((vaultTimeout: VaultTimeout) => { + // As a side effect, set the new value determined by determineVaultTimeout into state if it's different from the current + if (vaultTimeout !== currentVaultTimeout) { + return this.stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, userId); + } + }), + catchError((error: unknown) => { + // Protect outer observable from canceling on error by catching and returning EMPTY + this.logService.error(`Error getting vault timeout: ${error}`); + return EMPTY; + }), + ); + }), + distinctUntilChanged(), // Avoid having the set side effect trigger a new emission of the same action + shareReplay({ refCount: true, bufferSize: 1 }), + ); } - async getVaultTimeoutAction(userId?: UserId): Promise { - const availableActions = await this.getAvailableVaultTimeoutActions(); - if (availableActions.length === 1) { - return availableActions[0]; + private async determineVaultTimeout( + currentVaultTimeout: VaultTimeout | null, + maxVaultTimeoutPolicy: Policy | null, + ): Promise { + // if current vault timeout is null, apply the client specific default + currentVaultTimeout = currentVaultTimeout ?? this.defaultVaultTimeout; + + // If no policy applies, return the current vault timeout + if (!maxVaultTimeoutPolicy) { + return currentVaultTimeout; } - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId }); - const policies = await firstValueFrom( - this.policyService.getAll$(PolicyType.MaximumVaultTimeout, userId), + // User is subject to a max vault timeout policy + const maxVaultTimeoutPolicyData = maxVaultTimeoutPolicy.data; + + // If the current vault timeout is not numeric, change it to the policy compliant value + if (typeof currentVaultTimeout === "string") { + return maxVaultTimeoutPolicyData.minutes; + } + + // For numeric vault timeouts, ensure they are smaller than maximum allowed value according to policy + const policyCompliantTimeout = Math.min(currentVaultTimeout, maxVaultTimeoutPolicyData.minutes); + + return policyCompliantTimeout; + } + + private async setVaultTimeoutAction(userId: UserId, action: VaultTimeoutAction): Promise { + if (!userId) { + throw new Error("User id required. Cannot set vault timeout action."); + } + + if (!action) { + throw new Error("Vault Timeout Action cannot be null"); + } + + await this.stateProvider.setUserState(VAULT_TIMEOUT_ACTION, action, userId); + } + + getVaultTimeoutActionByUserId$(userId: UserId): Observable { + if (!userId) { + throw new Error("User id required. Cannot get vault timeout action."); + } + + return combineLatest([ + this.stateProvider.getUserState$(VAULT_TIMEOUT_ACTION, userId), + this.getMaxVaultTimeoutPolicyByUserId$(userId), + ]).pipe( + switchMap(([currentVaultTimeoutAction, maxVaultTimeoutPolicy]) => { + return from( + this.determineVaultTimeoutAction( + userId, + currentVaultTimeoutAction, + maxVaultTimeoutPolicy, + ), + ).pipe( + tap((vaultTimeoutAction: VaultTimeoutAction) => { + // As a side effect, set the new value determined by determineVaultTimeout into state if it's different from the current + // We want to avoid having a null timeout action always so we set it to the default if it is null + // and if the user becomes subject to a policy that requires a specific action, we set it to that + if (vaultTimeoutAction !== currentVaultTimeoutAction) { + return this.stateProvider.setUserState( + VAULT_TIMEOUT_ACTION, + vaultTimeoutAction, + userId, + ); + } + }), + catchError((error: unknown) => { + // Protect outer observable from canceling on error by catching and returning EMPTY + this.logService.error(`Error getting vault timeout: ${error}`); + return EMPTY; + }), + ); + }), + distinctUntilChanged(), // Avoid having the set side effect trigger a new emission of the same action + shareReplay({ refCount: true, bufferSize: 1 }), ); + } - if (policies?.length) { - const action = policies[0].data.action; - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - if (action && vaultTimeoutAction !== action) { - await this.stateService.setVaultTimeoutAction(action, { userId: userId }); - } - if (action && availableActions.includes(action)) { - return action; - } + private async determineVaultTimeoutAction( + userId: string, + currentVaultTimeoutAction: VaultTimeoutAction | null, + maxVaultTimeoutPolicy: Policy | null, + ): Promise { + const availableVaultTimeoutActions = await this.getAvailableVaultTimeoutActions(userId); + if (availableVaultTimeoutActions.length === 1) { + return availableVaultTimeoutActions[0]; } - if (vaultTimeoutAction == null) { - // Depends on whether or not the user has a master password - const defaultValue = (await this.userHasMasterPassword(userId)) - ? VaultTimeoutAction.Lock - : VaultTimeoutAction.LogOut; - // We really shouldn't need to set the value here, but multiple services relies on this value being correct. - await this.stateService.setVaultTimeoutAction(defaultValue, { userId: userId }); - return defaultValue; + if ( + maxVaultTimeoutPolicy?.data?.action && + availableVaultTimeoutActions.includes(maxVaultTimeoutPolicy.data.action) + ) { + // return policy defined vault timeout action + return maxVaultTimeoutPolicy.data.action; } - return vaultTimeoutAction === VaultTimeoutAction.LogOut - ? VaultTimeoutAction.LogOut - : VaultTimeoutAction.Lock; + // No policy applies from here on + // If the current vault timeout is null and lock is an option, set it as the default + if ( + currentVaultTimeoutAction == null && + availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock) + ) { + return VaultTimeoutAction.Lock; + } + + return currentVaultTimeoutAction; + } + + private getMaxVaultTimeoutPolicyByUserId$(userId: UserId): Observable { + if (!userId) { + throw new Error("User id required. Cannot get max vault timeout policy."); + } + + return this.policyService + .getAll$(PolicyType.MaximumVaultTimeout, userId) + .pipe(map((policies) => policies[0] ?? null)); } private async getAvailableVaultTimeoutActions(userId?: string): Promise { @@ -166,10 +289,9 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), ); - if (decryptionOptions?.hasMasterPassword != undefined) { - return decryptionOptions.hasMasterPassword; - } + return !!decryptionOptions?.hasMasterPassword; + } else { + return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } - return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } } diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.state.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.spec.ts new file mode 100644 index 0000000000..42a82e67ee --- /dev/null +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.spec.ts @@ -0,0 +1,36 @@ +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserKeyDefinition } from "../../platform/state"; +import { VaultTimeout } from "../../types/vault-timeout.type"; + +import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state"; + +describe.each([ + [VAULT_TIMEOUT_ACTION, VaultTimeoutAction.Lock], + [VAULT_TIMEOUT, 5], +])( + "deserializes state key definitions", + ( + keyDefinition: UserKeyDefinition | UserKeyDefinition, + state: VaultTimeoutAction | VaultTimeout | boolean, + ) => { + function getTypeDescription(value: any): string { + if (Array.isArray(value)) { + return "array"; + } else if (value === null) { + return "null"; + } + + // Fallback for primitive types + return typeof value; + } + + function testDeserialization(keyDefinition: UserKeyDefinition, state: T) { + const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state))); + expect(deserialized).toEqual(state); + } + + it(`should deserialize state for KeyDefinition<${getTypeDescription(state)}>: "${keyDefinition.key}"`, () => { + testDeserialization(keyDefinition, state); + }); + }, +); diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.state.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.ts new file mode 100644 index 0000000000..46097d6a4c --- /dev/null +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.state.ts @@ -0,0 +1,27 @@ +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserKeyDefinition, VAULT_TIMEOUT_SETTINGS_DISK_LOCAL } from "../../platform/state"; +import { VaultTimeout } from "../../types/vault-timeout.type"; + +/** + * Settings use disk storage and local storage on web so settings can persist after logout + * in order for us to know if the user's chose to never lock their vault or not. + * When the user has never lock selected, we have to set the user key in memory + * from the user auto unlock key stored on disk on client bootstrap. + */ +export const VAULT_TIMEOUT_ACTION = new UserKeyDefinition( + VAULT_TIMEOUT_SETTINGS_DISK_LOCAL, + "vaultTimeoutAction", + { + deserializer: (vaultTimeoutAction) => vaultTimeoutAction, + clearOn: [], // persisted on logout + }, +); + +export const VAULT_TIMEOUT = new UserKeyDefinition( + VAULT_TIMEOUT_SETTINGS_DISK_LOCAL, + "vaultTimeout", + { + deserializer: (vaultTimeout) => vaultTimeout, + clearOn: [], // persisted on logout + }, +); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 14b26fa541..51fca6e666 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -15,6 +15,7 @@ import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { CollectionService } from "../../vault/abstractions/collection.service"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; @@ -63,7 +64,9 @@ describe("VaultTimeoutService", () => { vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock); - vaultTimeoutSettingsService.vaultTimeoutAction$.mockReturnValue(vaultTimeoutActionSubject); + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue( + vaultTimeoutActionSubject, + ); availableVaultTimeoutActionsSubject = new BehaviorSubject([]); @@ -93,7 +96,7 @@ describe("VaultTimeoutService", () => { authStatus?: AuthenticationStatus; isAuthenticated?: boolean; lastActive?: number; - vaultTimeout?: number; + vaultTimeout?: VaultTimeout; timeoutAction?: VaultTimeoutAction; availableTimeoutActions?: VaultTimeoutAction[]; } @@ -121,8 +124,8 @@ describe("VaultTimeoutService", () => { return Promise.resolve(accounts[options.userId ?? globalSetups?.userId]?.isAuthenticated); }); - vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => { - return Promise.resolve(accounts[userId]?.vaultTimeout); + vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockImplementation((userId) => { + return new BehaviorSubject(accounts[userId]?.vaultTimeout); }); stateService.getUserId.mockResolvedValue(globalSetups?.userId); @@ -161,7 +164,7 @@ describe("VaultTimeoutService", () => { platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); - vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { + vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockImplementation((userId) => { return new BehaviorSubject(accounts[userId]?.timeoutAction); }); @@ -212,18 +215,18 @@ describe("VaultTimeoutService", () => { ); it.each([ - null, // never - -1, // onRestart - -2, // onLocked - -3, // onSleep - -4, // onIdle + VaultTimeoutStringType.Never, + VaultTimeoutStringType.OnRestart, + VaultTimeoutStringType.OnLocked, + VaultTimeoutStringType.OnSleep, + VaultTimeoutStringType.OnIdle, ])( "does not log out or lock a user who has %s as their vault timeout", async (vaultTimeout) => { setupAccounts({ 1: { authStatus: AuthenticationStatus.Unlocked, - vaultTimeout: vaultTimeout, + vaultTimeout: vaultTimeout as VaultTimeout, isAuthenticated: true, }, }); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index a75fb6d4c4..5d07c0ebd3 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -170,8 +170,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return false; } - const vaultTimeout = await this.vaultTimeoutSettingsService.getVaultTimeout(userId); - if (vaultTimeout == null || vaultTimeout < 0) { + const vaultTimeout = await firstValueFrom( + this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId), + ); + + if (typeof vaultTimeout === "string") { return false; } @@ -186,7 +189,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private async executeTimeoutAction(userId: UserId): Promise { const timeoutAction = await firstValueFrom( - this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId), + this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), ); timeoutAction === VaultTimeoutAction.LogOut ? await this.logOut(userId) diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index fea4f28b03..ed438cda88 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -59,13 +59,14 @@ import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-prov import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { KnownAccountsMigrator } from "./migrations/60-known-accounts"; import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers"; +import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 61; +export const CURRENT_VERSION = 62; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -128,7 +129,8 @@ export function createMigrationBuilder() { .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) .with(KdfConfigMigrator, 58, 59) .with(KnownAccountsMigrator, 59, 60) - .with(PinStateMigrator, 60, CURRENT_VERSION); + .with(PinStateMigrator, 60, 61) + .with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 21c5c72a18..9f6fa74747 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -242,6 +242,7 @@ export function mockMigrationHelper( mockHelper.remove.mockImplementation((key) => helper.remove(key)); mockHelper.type = helper.type; + mockHelper.clientType = helper.clientType; return mockHelper; } diff --git a/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.spec.ts new file mode 100644 index 0000000000..1a736c1623 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.spec.ts @@ -0,0 +1,669 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + ClientType, + VAULT_TIMEOUT, + VAULT_TIMEOUT_ACTION, + VaultTimeoutSettingsServiceStateProviderMigrator, +} from "./62-migrate-vault-timeout-settings-svc-to-state-provider"; + +// Represents data in state service pre-migration +function preMigrationJson() { + return { + global: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + // create the same structure for user3, user4, user5, user6, user7 in the global_account_accounts + user3: { + email: "user3@email.com", + name: "User 3", + emailVerified: true, + }, + user4: { + email: "user4@email.com", + name: "User 4", + emailVerified: true, + }, + user5: { + email: "user5@email.com", + name: "User 5", + emailVerified: true, + }, + user6: { + email: "user6@email.com", + name: "User 6", + emailVerified: true, + }, + user7: { + email: "user7@email.com", + name: "User 7", + emailVerified: true, + }, + }, + + user1: { + settings: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user2: { + settings: { + vaultTimeout: null as any, + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user3: { + settings: { + vaultTimeout: -1, // onRestart + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user4: { + settings: { + vaultTimeout: -2, // onLocked + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user5: { + settings: { + vaultTimeout: -3, // onSleep + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user6: { + settings: { + vaultTimeout: -4, // onIdle + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user7: { + settings: { + // no vault timeout data to migrate + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + }; +} + +function rollbackJSON(cli: boolean = false) { + const rollbackJson: any = { + // User specific state provider data + // use pattern user_{userId}_{stateDefinitionName}_{keyDefinitionKey} for user data + + // User1 migrated data + user_user1_vaultTimeoutSettings_vaultTimeout: 30, + user_user1_vaultTimeoutSettings_vaultTimeoutAction: "lock", + + // User2 migrated data + user_user2_vaultTimeoutSettings_vaultTimeout: "never", + user_user2_vaultTimeoutSettings_vaultTimeoutAction: "logOut", + + // User3 migrated data + user_user3_vaultTimeoutSettings_vaultTimeout: "onRestart", + user_user3_vaultTimeoutSettings_vaultTimeoutAction: "lock", + + // User4 migrated data + user_user4_vaultTimeoutSettings_vaultTimeout: "onLocked", + user_user4_vaultTimeoutSettings_vaultTimeoutAction: "logOut", + + // User5 migrated data + user_user5_vaultTimeoutSettings_vaultTimeout: "onSleep", + user_user5_vaultTimeoutSettings_vaultTimeoutAction: "lock", + + // User6 migrated data + user_user6_vaultTimeoutSettings_vaultTimeout: "onIdle", + user_user6_vaultTimeoutSettings_vaultTimeoutAction: "logOut", + + // User7 migrated data + // user_user7_vaultTimeoutSettings_vaultTimeout: null as any, + // user_user7_vaultTimeoutSettings_vaultTimeoutAction: null as any, + + // Global state provider data + // use pattern global_{stateDefinitionName}_{keyDefinitionKey} for global data + // Not migrating global data + + global: { + // no longer has vault timeout data + otherStuff: "otherStuff", + }, + + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + // create the same structure for user3, user4, user5, user6, user7 in the global_account_accounts + user3: { + email: "user3@email.com", + name: "User 3", + emailVerified: true, + }, + user4: { + email: "user4@email.com", + name: "User 4", + emailVerified: true, + }, + user5: { + email: "user5@email.com", + name: "User 5", + emailVerified: true, + }, + user6: { + email: "user6@email.com", + name: "User 6", + emailVerified: true, + }, + user7: { + email: "user7@email.com", + name: "User 7", + emailVerified: true, + }, + }, + + user1: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user2: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user3: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user4: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user5: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user6: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + user7: { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }, + }; + + if (cli) { + rollbackJson.user_user7_vaultTimeoutSettings_vaultTimeout = "never"; + } + + return rollbackJson; +} + +describe("VaultTimeoutSettingsServiceStateProviderMigrator", () => { + let helper: MockProxy; + let sut: VaultTimeoutSettingsServiceStateProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 61); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); + + // Global data + expect(helper.set).toHaveBeenCalledWith("global", { + // no longer has vault timeout data + otherStuff: "otherStuff", + }); + + // User data + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledTimes(7); // 6 users + 1 global + expect(helper.set).not.toHaveBeenCalledWith("user7", any()); + }); + + it("should migrate data to state providers for defined accounts that have the data", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, 30); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, "never"); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, "onRestart"); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, "onLocked"); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, "onSleep"); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, "onIdle"); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, "logOut"); + + // Expect that we didn't migrate anything to user 7 or 8 + expect(helper.setToUser).not.toHaveBeenCalledWith("user7", VAULT_TIMEOUT, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT_ACTION, any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 62); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should null out newly migrated entries in state provider framework", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, null); + }); + + it("should add back data to all accounts that had migrated data (only user 1)", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + vaultTimeout: null, + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + vaultTimeout: -1, // onRestart + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + vaultTimeout: -2, // onLocked + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + vaultTimeout: -3, // onSleep + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + vaultTimeout: -4, // onIdle + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + }); + + it("should not add back the global vault timeout data", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("global", any()); + }); + + it("should not add data back if data wasn't migrated or acct doesn't exist", async () => { + await sut.rollback(helper); + + // no data to add back for user7 (acct exists but no migrated data) and user8 (no acct) + expect(helper.set).not.toHaveBeenCalledWith("user7", any()); + expect(helper.set).not.toHaveBeenCalledWith("user8", any()); + }); + }); +}); + +describe("VaultTimeoutSettingsServiceStateProviderMigrator - CLI", () => { + let helper: MockProxy; + let sut: VaultTimeoutSettingsServiceStateProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 61, "general", ClientType.Cli); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); + + // Global data + expect(helper.set).toHaveBeenCalledWith("global", { + // no longer has vault timeout data + otherStuff: "otherStuff", + }); + + // User data + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user7", { + settings: { + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledTimes(8); // 7 users + 1 global + expect(helper.set).not.toHaveBeenCalledWith("user8", any()); + }); + + it("should migrate data to state providers for defined accounts that have the data with an exception for the vault timeout", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, 30); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, "never"); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, "onRestart"); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, "onLocked"); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, "logOut"); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, "onSleep"); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, "lock"); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, "onIdle"); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, "logOut"); + + // User7 has an undefined vault timeout, but we should still migrate it to "never" + // b/c the CLI doesn't have a vault timeout + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT, "never"); + // Note: we don't have to worry about not migrating the vault timeout action b/c each client + // has a default value for the vault timeout action when it is retrieved via the vault timeout settings svc. + expect(helper.setToUser).not.toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, any()); + + // Expect that we didn't migrate anything to user 8 b/c it doesn't exist + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user8", VAULT_TIMEOUT_ACTION, any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(true), 62, "general", ClientType.Cli); + sut = new VaultTimeoutSettingsServiceStateProviderMigrator(61, 62); + }); + + it("should null out newly migrated entries in state provider framework", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user1", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user2", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user3", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user4", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user5", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user6", VAULT_TIMEOUT_ACTION, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT, null); + expect(helper.setToUser).toHaveBeenCalledWith("user7", VAULT_TIMEOUT_ACTION, null); + }); + + it("should add back data to all accounts that had migrated data (only user 1)", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + settings: { + vaultTimeout: 30, + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user2", { + settings: { + vaultTimeout: null, + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user3", { + settings: { + vaultTimeout: -1, // onRestart + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user4", { + settings: { + vaultTimeout: -2, // onLocked + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user5", { + settings: { + vaultTimeout: -3, // onSleep + vaultTimeoutAction: "lock", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user6", { + settings: { + vaultTimeout: -4, // onIdle + vaultTimeoutAction: "logOut", + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + + expect(helper.set).toHaveBeenCalledWith("user7", { + settings: { + vaultTimeout: null, + // vaultTimeoutAction: null, // not migrated + otherStuff: "otherStuff", + }, + otherStuff: "otherStuff", + }); + }); + + it("should not add back the global vault timeout data", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("global", any()); + }); + + it("should not add data back if data wasn't migrated or acct doesn't exist", async () => { + await sut.rollback(helper); + + // no data to add back for user8 (no acct) + expect(helper.set).not.toHaveBeenCalledWith("user8", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts new file mode 100644 index 0000000000..ee9ee4c9ea --- /dev/null +++ b/libs/common/src/state-migrations/migrations/62-migrate-vault-timeout-settings-svc-to-state-provider.ts @@ -0,0 +1,174 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +// Types to represent data as it is stored in JSON +type ExpectedAccountType = { + settings?: { + vaultTimeout?: number; + vaultTimeoutAction?: string; + }; +}; + +type ExpectedGlobalType = { + vaultTimeout?: number; + vaultTimeoutAction?: string; +}; + +const VAULT_TIMEOUT_SETTINGS_STATE_DEF_LIKE: StateDefinitionLike = { + name: "vaultTimeoutSettings", +}; + +export const VAULT_TIMEOUT: KeyDefinitionLike = { + key: "vaultTimeout", // matches KeyDefinition.key + stateDefinition: VAULT_TIMEOUT_SETTINGS_STATE_DEF_LIKE, +}; + +export const VAULT_TIMEOUT_ACTION: KeyDefinitionLike = { + key: "vaultTimeoutAction", // matches KeyDefinition.key + stateDefinition: VAULT_TIMEOUT_SETTINGS_STATE_DEF_LIKE, +}; + +// Migrations are supposed to be frozen so we have to copy the type here. +export type VaultTimeout = + | number // 0 for immediately; otherwise positive numbers + | "never" // null + | "onRestart" // -1 + | "onLocked" // -2 + | "onSleep" // -3 + | "onIdle"; // -4 + +// Define mapping of old values to new values for migration purposes +const vaultTimeoutTypeMigrateRecord: Record = { + null: "never", + "-1": "onRestart", + "-2": "onLocked", + "-3": "onSleep", + "-4": "onIdle", +}; + +// define mapping of new values to old values for rollback purposes +const vaultTimeoutTypeRollbackRecord: Record = { + never: null, + onRestart: -1, + onLocked: -2, + onSleep: -3, + onIdle: -4, +}; + +export enum ClientType { + Web = "web", + Browser = "browser", + Desktop = "desktop", + Cli = "cli", +} + +export class VaultTimeoutSettingsServiceStateProviderMigrator extends Migrator<61, 62> { + async migrate(helper: MigrationHelper): Promise { + const globalData = await helper.get("global"); + + const accounts = await helper.getAccounts(); + async function migrateAccount( + userId: string, + account: ExpectedAccountType | undefined, + ): Promise { + let updatedAccount = false; + + // Migrate vault timeout + let existingVaultTimeout = account?.settings?.vaultTimeout; + + if (helper.clientType === ClientType.Cli && existingVaultTimeout === undefined) { + // The CLI does not set a vault timeout by default so we need to set it to null + // so that the migration can migrate null to "never" as the CLI does not have a vault timeout. + existingVaultTimeout = null; + } + + if (existingVaultTimeout !== undefined) { + // check undefined so that we allow null values (previously meant never timeout) + // Only migrate data that exists + + if (existingVaultTimeout === null || existingVaultTimeout < 0) { + // Map null or negative values to new string values + const newVaultTimeout = vaultTimeoutTypeMigrateRecord[existingVaultTimeout]; + await helper.setToUser(userId, VAULT_TIMEOUT, newVaultTimeout); + } else { + // Persist positive numbers as is + await helper.setToUser(userId, VAULT_TIMEOUT, existingVaultTimeout); + } + + delete account?.settings?.vaultTimeout; + updatedAccount = true; + } + + // Migrate vault timeout action + const existingVaultTimeoutAction = account?.settings?.vaultTimeoutAction; + + if (existingVaultTimeoutAction != null) { + // Only migrate data that exists + await helper.setToUser(userId, VAULT_TIMEOUT_ACTION, existingVaultTimeoutAction); + + delete account?.settings?.vaultTimeoutAction; + updatedAccount = true; + } + + // Note: we are explicitly not worrying about mapping over the global fallback vault timeout / action + // into the new state provider framework. It was originally a fallback but hasn't been used for years + // so this migration will clean up the global properties fully. + + if (updatedAccount) { + // Save the migrated account only if it was updated + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + + // Delete global data + delete globalData?.vaultTimeout; + delete globalData?.vaultTimeoutAction; + await helper.set("global", globalData); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedLegacyAccount = false; + + // Rollback vault timeout + const migratedVaultTimeout = await helper.getFromUser(userId, VAULT_TIMEOUT); + + if (account?.settings && migratedVaultTimeout != null) { + if (typeof migratedVaultTimeout === "string") { + // Map new string values back to old values + account.settings.vaultTimeout = vaultTimeoutTypeRollbackRecord[migratedVaultTimeout]; + } else { + // persist numbers as is + account.settings.vaultTimeout = migratedVaultTimeout; + } + + updatedLegacyAccount = true; + } + + await helper.setToUser(userId, VAULT_TIMEOUT, null); + + // Rollback vault timeout action + const migratedVaultTimeoutAction = await helper.getFromUser( + userId, + VAULT_TIMEOUT_ACTION, + ); + + if (account?.settings && migratedVaultTimeoutAction != null) { + account.settings.vaultTimeoutAction = migratedVaultTimeoutAction; + updatedLegacyAccount = true; + } + + await helper.setToUser(userId, VAULT_TIMEOUT_ACTION, null); + + if (updatedLegacyAccount) { + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/types/vault-timeout.type.ts b/libs/common/src/types/vault-timeout.type.ts new file mode 100644 index 0000000000..e5a2e7f182 --- /dev/null +++ b/libs/common/src/types/vault-timeout.type.ts @@ -0,0 +1,17 @@ +// Note: the below comments are just for documenting what they used to be. +export const VaultTimeoutStringType = { + Never: "never", // null + OnRestart: "onRestart", // -1 + OnLocked: "onLocked", // -2 + OnSleep: "onSleep", // -3 + OnIdle: "onIdle", // -4 +} as const; + +export type VaultTimeout = + | number // 0 or positive numbers only + | (typeof VaultTimeoutStringType)[keyof typeof VaultTimeoutStringType]; + +export interface VaultTimeoutOption { + name: string; + value: VaultTimeout; +} From 626128d49858844db9c24862f88e7e46fe00b2af Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Mon, 13 May 2024 16:49:31 -0400 Subject: [PATCH 18/33] make manifest v3 the default for build and build:watch commands (#9160) --- apps/browser/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 278a3b6c52..1f54bd64ac 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -2,10 +2,10 @@ "name": "@bitwarden/browser", "version": "2024.5.0", "scripts": { - "build": "webpack", - "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", - "build:watch": "webpack --watch", - "build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch", + "build": "cross-env MANIFEST_VERSION=3 webpack", + "build:mv2": "webpack", + "build:watch": "cross-env MANIFEST_VERSION=3 webpack --watch", + "build:watch:mv2": "webpack --watch", "build:prod": "cross-env NODE_ENV=production webpack", "build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch", From bf57a181eb40f1978153c5bd84bba6564dae77f7 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 13 May 2024 22:00:38 +0100 Subject: [PATCH 19/33] [AC-2570] Existing providers see new CB experience on Admin Console org Billing Subscription page (#9108) * Fix the subscription page issue for existing providers * Merge branch 'main' into ac-2570-existing-providers-see-new-cb-experience-on-admin-console-org-billing-subscription-page * Fix pr comment on error if the user isn't a provider user * Resolve the pr comment on error for non provider user * Remove unused property --- .../organization-subscription-cloud.component.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index d021c827a7..a0db7b5a20 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -5,7 +5,8 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { OrganizationApiKeyType, ProviderStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -69,6 +70,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private route: ActivatedRoute, private dialogService: DialogService, private configService: ConfigService, + private providerService: ProviderService, ) {} async ngOnInit() { @@ -109,8 +111,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy if (this.userOrg.canViewSubscription) { const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - - this.isProviderManaged = enableConsolidatedBilling && this.userOrg.hasProvider; + const provider = await this.providerService.get(this.userOrg.providerId); + this.isProviderManaged = + enableConsolidatedBilling && + this.userOrg.hasProvider && + provider.providerStatus == ProviderStatusType.Billable; this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; From 66f5d908035554960b350e534f2c7a05220b3ee7 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 13 May 2024 17:04:26 -0400 Subject: [PATCH 20/33] PM-5501 - VaultTimeoutSettingsSvc State Provider Migration - Small bugfixes (#9164) * PM-5501 - VaultTimeoutSettingsSvc - fix setVaultTimeoutOptions condition which needed to use never instead of null. * PM-5501 - Fix browser and desktop not showing the never lock warning * PM-5501 - Use true equality. --- .../popup/settings/account-security.component.ts | 4 ++-- .../desktop/src/app/accounts/settings.component.ts | 2 +- .../vault-timeout-settings.service.spec.ts | 14 +++++++++++++- .../vault-timeout-settings.service.ts | 4 ++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index ea7c3a5e8d..32cfbe416d 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -254,7 +254,7 @@ export class AccountSecurityComponent implements OnInit { } async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) { - if (newValue == null) { + if (newValue === VaultTimeoutStringType.Never) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, content: { key: "neverLockWarning" }, @@ -289,7 +289,7 @@ export class AccountSecurityComponent implements OnInit { newValue, vaultTimeoutAction, ); - if (newValue == null) { + if (newValue === VaultTimeoutStringType.Never) { this.messagingService.send("bgReseedStorage"); } } diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index eedf072a81..ade019b9fb 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -371,7 +371,7 @@ export class SettingsComponent implements OnInit { } async saveVaultTimeout(newValue: VaultTimeout) { - if (newValue == null) { + if (newValue === VaultTimeoutStringType.Never) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, content: { key: "neverLockWarning" }, diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 894d550bda..e9839fc4e6 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -313,7 +313,7 @@ describe("VaultTimeoutSettingsService", () => { expect(cryptoService.refreshAdditionalKeys).toHaveBeenCalled(); }); - it("should clear the tokens when the timeout is non-null and the action is log out", async () => { + it("should clear the tokens when the timeout is not never and the action is log out", async () => { // Arrange const action = VaultTimeoutAction.LogOut; const timeout = 30; @@ -324,6 +324,18 @@ describe("VaultTimeoutSettingsService", () => { // Assert expect(tokenService.clearTokens).toHaveBeenCalled(); }); + + it("should not clear the tokens when the timeout is never and the action is log out", async () => { + // Arrange + const action = VaultTimeoutAction.LogOut; + const timeout = VaultTimeoutStringType.Never; + + // Act + await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action); + + // Assert + expect(tokenService.clearTokens).not.toHaveBeenCalled(); + }); }); function createVaultTimeoutSettingsService( diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index 282b86fb63..d48729e9c6 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -30,7 +30,7 @@ import { LogService } from "../../platform/abstractions/log.service"; import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; import { StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; -import { VaultTimeout } from "../../types/vault-timeout.type"; +import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state"; @@ -74,7 +74,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA await this.setVaultTimeout(userId, timeout); - if (timeout != null && action === VaultTimeoutAction.LogOut) { + if (timeout != VaultTimeoutStringType.Never && action === VaultTimeoutAction.LogOut) { // if we have a vault timeout and the action is log out, reset tokens // as the tokens were stored on disk and now should be stored in memory await this.tokenService.clearTokens(); From 3900924250f820900c63ef68a99fd456c0eb1d36 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 13 May 2024 16:13:27 -0500 Subject: [PATCH 21/33] [AC-2086] Update CanDelete with v1 flag logic (#9100) * feat: update org domain object deleteAnyCollection with v1 flag logic, refs AC-2086 * feat: update canDelete method to handle v1 flag logic, refs AC-2086 * feat: update canDelete references to pass v1 flag, refs AC-2086 * feat: add provider check and modify owner/admin type checks, refs AC-2086 * fix: add permission to org instantiation for vault item stories, refs AC-2086 --- .../collection-dialog.component.ts | 4 +++- .../vault-items/vault-items.component.ts | 2 +- .../vault-items/vault-items.stories.ts | 2 ++ .../vault/core/views/collection-admin.view.ts | 6 ----- .../bulk-delete-dialog.component.ts | 7 ++++-- .../vault-header/vault-header.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 3 ++- .../vault-header/vault-header.component.ts | 2 +- .../app/vault/org-vault/vault.component.ts | 2 +- .../models/domain/organization.ts | 23 ++++++++++++++++--- .../src/vault/models/view/collection.view.ts | 10 ++++---- 11 files changed, 42 insertions(+), 21 deletions(-) diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 4e95bb4bcc..a0d7617ce8 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -214,7 +214,9 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { access: accessSelections, }); this.collection.manage = collection?.manage ?? false; // Get manage flag from sync data collection - this.showDeleteButton = !this.dialogReadonly && this.collection.canDelete(organization); + this.showDeleteButton = + !this.dialogReadonly && + this.collection.canDelete(organization, flexibleCollectionsV1); } else { this.nestOptions = collections; const parent = collections.find((c) => c.id === this.params.parentCollectionId); diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index f172a73b06..8fcf5b01b1 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -163,7 +163,7 @@ export class VaultItemsComponent { } } - return collection.canDelete(organization); + return collection.canDelete(organization, this.flexibleCollectionsV1Enabled); } protected canViewCollectionInfo(collection: CollectionView) { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index 41aa766e3a..c7cc85a37b 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -4,6 +4,7 @@ import { applicationConfig, Meta, moduleMetadata, Story } from "@storybook/angul import { BehaviorSubject, of } from "rxjs"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -318,5 +319,6 @@ function createOrganization(i: number): Organization { organization.id = `organization-${i}`; organization.name = `Organization ${i}`; organization.type = OrganizationUserType.Owner; + organization.permissions = new PermissionsApi(); return organization; } diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index b8725688ce..ad23584f6c 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -71,12 +71,6 @@ export class CollectionAdminView extends CollectionView { (org?.canEditAssignedCollections && this.assigned); } - override canDelete(org: Organization): boolean { - return org?.flexibleCollections - ? org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage) - : org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned); - } - /** * Whether the user can modify user access to this collection */ diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index f49c54ac32..ee036f5e3b 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -145,9 +145,12 @@ export class BulkDeleteDialogComponent { } private async deleteCollections(): Promise { + const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); // From org vault if (this.organization) { - if (this.collections.some((c) => !c.canDelete(this.organization))) { + if ( + this.collections.some((c) => !c.canDelete(this.organization, flexibleCollectionsV1Enabled)) + ) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), @@ -164,7 +167,7 @@ export class BulkDeleteDialogComponent { const deletePromises: Promise[] = []; for (const organization of this.organizations) { const orgCollections = this.collections.filter((o) => o.organizationId === organization.id); - if (orgCollections.some((c) => !c.canDelete(organization))) { + if (orgCollections.some((c) => !c.canDelete(organization, flexibleCollectionsV1Enabled))) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 08afd09982..9e69286277 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -176,7 +176,7 @@ export class VaultHeaderComponent implements OnInit { (o) => o.id === this.collection?.node.organizationId, ); - return this.collection.node.canDelete(organization); + return this.collection.node.canDelete(organization, this.flexibleCollectionsV1Enabled); } deleteCollection() { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index ca3525effa..49565bdcee 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -693,7 +693,8 @@ export class VaultComponent implements OnInit, OnDestroy { async deleteCollection(collection: CollectionView): Promise { const organization = await this.organizationService.get(collection.organizationId); - if (!collection.canDelete(organization)) { + const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); + if (!collection.canDelete(organization, flexibleCollectionsV1Enabled)) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 9cac154d20..58af5516cc 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -207,7 +207,7 @@ export class VaultHeaderComponent implements OnInit { } // Otherwise, check if we can delete the specified collection - return this.collection.node.canDelete(this.organization); + return this.collection.node.canDelete(this.organization, this.flexibleCollectionsV1Enabled); } get canViewCollectionInfo(): boolean { diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index f35c3b41bb..64823c9754 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -1047,7 +1047,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async deleteCollection(collection: CollectionView): Promise { - if (!collection.canDelete(this.organization)) { + if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 04840477df..752b792844 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -232,8 +232,23 @@ export class Organization { ); } - get canDeleteAnyCollection() { - return this.isAdmin || this.permissions.deleteAnyCollection; + /** + * @param flexibleCollectionsV1Enabled - Whether or not the V1 Flexible Collection feature flag is enabled + * @returns True if the user can delete any collection + */ + canDeleteAnyCollection(flexibleCollectionsV1Enabled: boolean) { + // Providers and Users with DeleteAnyCollection permission can always delete collections + if (this.isProviderUser || this.permissions.deleteAnyCollection) { + return true; + } + + // If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionCreationDeletion setting + // Using explicit type checks because provider users are handled above and this mimics the server's permission checks closely + if (!flexibleCollectionsV1Enabled || this.allowAdminAccessToAllCollectionItems) { + return this.type == OrganizationUserType.Owner || this.type == OrganizationUserType.Admin; + } + + return false; } /** @@ -242,7 +257,9 @@ export class Organization { */ get canViewAllCollections() { // Admins can always see all collections even if collection management settings prevent them from editing them or seeing items - return this.isAdmin || this.permissions.editAnyCollection || this.canDeleteAnyCollection; + return ( + this.isAdmin || this.permissions.editAnyCollection || this.permissions.deleteAnyCollection + ); } /** diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 991f9abe55..5e49d0a6c0 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -75,16 +75,18 @@ export class CollectionView implements View, ITreeNodeObject { } // For deleting a collection, not the items within it. - canDelete(org: Organization): boolean { + canDelete(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { if (org != null && org.id !== this.organizationId) { throw new Error( "Id of the organization provided does not match the org id of the collection.", ); } - return org?.flexibleCollections - ? org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage) - : org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections; + const canDeleteManagedCollections = !org?.limitCollectionCreationDeletion || org.isAdmin; + return ( + org?.canDeleteAnyCollection(flexibleCollectionsV1Enabled) || + (canDeleteManagedCollections && this.manage) + ); } /** From 8e4073f1cafa5348fd5120003411b1ac673667a8 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 13 May 2024 14:18:54 -0700 Subject: [PATCH 22/33] fix migration test (#9163) --- .../auth/src/common/services/pin/pin.service.implementation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index 9e2e575eeb..61586fae46 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -387,9 +387,10 @@ export class PinService implements PinServiceAbstraction { ); const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId }); + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( masterKey, - new EncString(encUserKey), + encUserKey ? new EncString(encUserKey) : undefined, ); const pinKeyEncryptedUserKey = await this.createPinKeyEncryptedUserKey(pin, userKey, userId); From 8c5841a76aa68375f43bf6b38554cd1e3a92dd63 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 14 May 2024 08:30:02 +1000 Subject: [PATCH 23/33] [AC-2500] Collection row menus - adjust feature flagging (#9116) --- .../vault-items/vault-collection-row.component.html | 4 +--- .../components/vault-items/vault-items.component.ts | 2 +- .../src/app/vault/core/views/collection-admin.view.ts | 9 ++++++++- .../org-vault/vault-header/vault-header.component.html | 10 ++++++---- .../org-vault/vault-header/vault-header.component.ts | 5 ++++- libs/common/src/vault/models/view/collection.view.ts | 5 ++++- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index 7bad783f19..4f9e65454b 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -83,9 +83,7 @@ {{ "access" | i18n }} - + + `, }) export class CollectionAccessRestrictedComponent { protected icon = icon; + protected collectionDialogTabType = CollectionDialogTabType; @Input() canEditCollection = false; + @Input() canViewCollectionInfo = false; - @Output() viewCollectionClicked = new EventEmitter(); - - get buttonText() { - return this.canEditCollection ? "editCollection" : "viewCollection"; - } + @Output() viewCollectionClicked = new EventEmitter<{ + readonly: boolean; + tab: CollectionDialogTabType; + }>(); } diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index a437ac2092..2b30bce611 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -116,13 +116,14 @@ diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c626c3c80e..3931d122e1 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7740,9 +7740,6 @@ "success": { "message": "Success" }, - "viewCollection": { - "message": "View collection" - }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, From 16971be52de9f1835b976dc8b2d61df58768cc80 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 14 May 2024 09:02:26 +1000 Subject: [PATCH 25/33] Fix restricted access view not loading for providers (#9165) --- apps/web/src/app/vault/org-vault/vault.component.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 2b30bce611..a6d1cd3074 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -117,10 +117,13 @@ + + + {{ "updateName" | i18n }} + {{ dialogParams.organization.name }} + +
+ + + {{ "organizationName" | i18n }} + + + +
+ + + + +
+ diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-name.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-name.component.ts new file mode 100644 index 0000000000..81e01a66cb --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-name.component.ts @@ -0,0 +1,77 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; +import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +type ManageClientOrganizationNameParams = { + providerId: string; + organization: { + id: string; + name: string; + seats: number; + }; +}; + +export enum ManageClientOrganizationNameResultType { + Closed = "closed", + Submitted = "submitted", +} + +export const openManageClientOrganizationNameDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig, +) => + dialogService.open( + ManageClientOrganizationNameComponent, + dialogConfig, + ); + +@Component({ + selector: "app-manage-client-organization-name", + templateUrl: "manage-client-organization-name.component.html", +}) +export class ManageClientOrganizationNameComponent { + protected ResultType = ManageClientOrganizationNameResultType; + protected formGroup = this.formBuilder.group({ + name: [this.dialogParams.organization.name, Validators.required], + }); + + constructor( + @Inject(DIALOG_DATA) protected dialogParams: ManageClientOrganizationNameParams, + private billingApiService: BillingApiServiceAbstraction, + private dialogRef: DialogRef, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private toastService: ToastService, + ) {} + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + const request = new UpdateClientOrganizationRequest(); + request.assignedSeats = this.dialogParams.organization.seats; + request.name = this.formGroup.value.name; + + await this.billingApiService.updateClientOrganization( + this.dialogParams.providerId, + this.dialogParams.organization.id, + request, + ); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedOrganizationName"), + }); + + this.dialogRef.close(this.ResultType.Submitted); + }; +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts index 2182ac43ab..3b05476777 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts @@ -71,6 +71,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit { const request = new UpdateClientOrganizationRequest(); request.assignedSeats = assignedSeats; + request.name = this.clientName; await this.billingApiService.updateClientOrganization( this.providerId, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html index ec5df609c4..d2f8ab7a85 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html @@ -78,8 +78,12 @@ appA11yTitle="{{ 'options' | i18n }}" > +