From fb577a448ea402bb02e5fa5d690119c06723d6a6 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Fri, 31 May 2024 13:36:04 -0400 Subject: [PATCH 01/28] Bumped client version(s) (#9464) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 90d9841a61..f84f3e5949 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.5.0", + "version": "2024.5.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 508c42fa72..39899a1e6b 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.5.0", + "version": "2024.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.5.0", + "version": "2024.5.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index ea4b95491c..9efd5bad9f 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.5.0", + "version": "2024.5.1", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 856972271e..a1acd1ea8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -237,7 +237,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.5.0", + "version": "2024.5.1", "hasInstallScript": true, "license": "GPL-3.0" }, From 00c305dff363405c367ef4f112b098da757aca00 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 31 May 2024 13:21:45 -0500 Subject: [PATCH 02/28] Browser - Add `jest-preset-angular` (#9412) * add "jest-preset-angular/setup-jest" to browser app * use flushPromises rather than await a dummy promise * move the import of `page-script` into beforeAll rather than the describe scope * invoke the sendMessage callback rather than relying on a promise comparison --- .../autofill/content/autofill-init.spec.ts | 2 +- apps/browser/src/autofill/utils/index.spec.ts | 25 +++++++++++++++---- .../page-script.webauthn-supported.spec.ts | 4 ++- apps/browser/test.setup.ts | 2 ++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index 4ea66edb3e..302b520e33 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -144,7 +144,7 @@ describe("AutofillInit", () => { .mockResolvedValue(pageDetails); const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await Promise.resolve(response); + await flushPromises(); expect(response).toBe(true); expect(sendResponse).toHaveBeenCalledWith(pageDetails); diff --git a/apps/browser/src/autofill/utils/index.spec.ts b/apps/browser/src/autofill/utils/index.spec.ts index af67d41601..dcb5aa6469 100644 --- a/apps/browser/src/autofill/utils/index.spec.ts +++ b/apps/browser/src/autofill/utils/index.spec.ts @@ -37,14 +37,29 @@ describe("generateRandomCustomElementName", () => { }); describe("sendExtensionMessage", () => { - it("sends a message to the extention", () => { - const extensionMessageResponse = sendExtensionMessage("updateAutofillOverlayHidden", { + it("sends a message to the extension", async () => { + const extensionMessagePromise = sendExtensionMessage("updateAutofillOverlayHidden", { display: "none", }); - jest.spyOn(chrome.runtime, "sendMessage"); - expect(chrome.runtime.sendMessage).toHaveBeenCalled(); - expect(extensionMessageResponse).toEqual(Promise.resolve({})); + // Jest doesn't give anyway to select the typed overload of "sendMessage", + // a cast is needed to get the correct spy type. + const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage") as unknown as jest.SpyInstance< + void, + [message: string, responseCallback: (response: string) => void], + unknown + >; + + expect(sendMessageSpy).toHaveBeenCalled(); + + const [latestCall] = sendMessageSpy.mock.calls; + const responseCallback = latestCall[1]; + + responseCallback("sendMessageResponse"); + + const response = await extensionMessagePromise; + + expect(response).toEqual("sendMessageResponse"); }); }); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts index c235d53cb0..3044d51fa7 100644 --- a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts @@ -53,7 +53,9 @@ describe("Fido2 page script with native WebAuthn support", () => { const mockCredentialAssertResult = createAssertCredentialResultMock(); setupMockedWebAuthnSupport(); - require("./page-script"); + beforeAll(() => { + require("./page-script"); + }); afterEach(() => { jest.resetModules(); diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 4800b4c17f..5435c6fd7f 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -1,3 +1,5 @@ +import "jest-preset-angular/setup-jest"; + // Add chrome storage api const QUOTA_BYTES = 10; const storage = { From 107ac5dc38c0858d8ba60e2750627b5230dabfd2 Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Fri, 31 May 2024 20:58:10 +0100 Subject: [PATCH 03/28] Upgrade to macos-13 runner (#9457) --- .github/workflows/build-cli.yml | 2 +- .github/workflows/build-desktop.yml | 28 +++++++--------------- .github/workflows/release-desktop-beta.yml | 21 +++++----------- 3 files changed, 15 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 47ebaf5189..6809d950c4 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -65,7 +65,7 @@ jobs: os: [ { base: "linux", distro: "ubuntu-22.04" }, - { base: "mac", distro: "macos-11" } + { base: "mac", distro: "macos-13" } ] license_type: [ diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index e73f882bb4..bebf7b5646 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -444,10 +444,7 @@ jobs: macos-build: name: MacOS Build - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} @@ -605,10 +602,7 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - browser-build - macos-build @@ -814,10 +808,7 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - browser-build - macos-build @@ -1014,11 +1005,7 @@ jobs: macos-package-dev: name: MacOS Package Dev Release Asset - if: false # We need to look into how code signing works for dev - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - browser-build - macos-build @@ -1188,14 +1175,15 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Zip masdev asset - working-directory: ./dist/mas-dev-universal - run: zip -r Bitwarden-${{ env.PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app + run: | + cd dist/mas-dev-universal + zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip - path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip + path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip if-no-files-found: error diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 46f4ffad57..55f822e623 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -393,10 +393,7 @@ jobs: macos-build: name: MacOS Build - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} @@ -457,7 +454,7 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 - + az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 @@ -525,10 +522,7 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - setup - macos-build @@ -596,7 +590,7 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 - + az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 @@ -738,10 +732,7 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - setup - macos-build @@ -804,7 +795,7 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 - + az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 From b457962850bbed9b9018002f26c53ee04982eab0 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Fri, 31 May 2024 17:59:39 -0400 Subject: [PATCH 04/28] [PM-6788] enable bugfixes in babel/preset-env (#9465) * enable bugfixes in babel/preset-env --- babel.config.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/babel.config.json b/babel.config.json index 7f4611dec0..4d817f0abf 100644 --- a/babel.config.json +++ b/babel.config.json @@ -1,4 +1,11 @@ { - "presets": ["@babel/preset-env"], + "presets": [ + [ + "@babel/preset-env", + { + "bugfixes": true + } + ] + ], "plugins": ["@angular/compiler-cli/linker/babel"] } From 0d492b4454f3e5672ae0c4a25ea1600ec44ee6ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Sat, 1 Jun 2024 19:07:36 +0200 Subject: [PATCH 05/28] Fix PM-8134: Change b64 to b64url (#9193) --- .../request/webauthn-login-attestation-response.request.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts b/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts index ef3d657f2f..f7c391b0ee 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts @@ -20,8 +20,8 @@ export class WebauthnLoginAttestationResponseRequest extends WebauthnLoginAuthen } this.response = { - attestationObject: Utils.fromBufferToB64(credential.response.attestationObject), - clientDataJson: Utils.fromBufferToB64(credential.response.clientDataJSON), + attestationObject: Utils.fromBufferToUrlB64(credential.response.attestationObject), + clientDataJson: Utils.fromBufferToUrlB64(credential.response.clientDataJSON), }; } } From f172612b1951966de878a4978136d4d8c9c4c9d8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 2 Jun 2024 09:30:49 +0000 Subject: [PATCH 06/28] Autosync the updated translations (#9470) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/az/messages.json | 24 +++++++------- apps/browser/src/_locales/de/messages.json | 2 +- apps/browser/src/_locales/lt/messages.json | 32 +++++++++---------- apps/browser/src/_locales/sv/messages.json | 8 ++--- apps/browser/src/_locales/uk/messages.json | 24 +++++++------- apps/browser/src/_locales/zh_CN/messages.json | 4 +-- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 76615435f7..129aac00ee 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -763,7 +763,7 @@ "message": "Kilidi aç" }, "additionalOptions": { - "message": "Additional options" + "message": "Əlavə seçimlər" }, "enableContextMenuItem": { "message": "Konteks menyu seçimlərini göstər" @@ -803,7 +803,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Buradan xaricə köçür" }, "exportVault": { "message": "Anbarı xaricə köçür" @@ -812,28 +812,28 @@ "message": "Fayl formatı" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Bu faylın xaricə köçürülməsi, parolla qorunacaq və şifrəsini açmaq üçün fayl parolu tələb olunacaq." }, "filePassword": { - "message": "File password" + "message": "Fayl parolu" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Bu parol, bu faylı daxilə və xaricə köçürmək üçün istifadə olunacaq" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Xaricə köçürməni şifrələmək və daxilə köçürməni yalnız mövcud Bitwarden hesabı ilə məhdudlaşdırmaq üçün hesabınızın istifadəçi adı və Ana Parolundan əldə edilən hesab şifrələmə açarınızı istifadə edin." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Xaricə köçürməni şifrələmək üçün bir fayl parolu təyin edin və şifrəni açma parolunu istifadə edərək bunu istənilən Bitwarden hesabına köçürün." }, "exportTypeHeading": { - "message": "Export type" + "message": "Xaricə köçürmə növü" }, "accountRestricted": { - "message": "Account restricted" + "message": "Hesab məhdudlaşdırıldı" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Fayl parolu\" və \"Fayl parolunu təsdiqlə\" uyuşmur." }, "warning": { "message": "XƏBƏRDARLIQ", @@ -2213,10 +2213,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Təşkilat anbarını xaricə köçürmə" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Yalnız $ORGANIZATION$ ilə əlaqələndirilmiş təşkilat anbarı ixrac ediləcək. Fərdi anbardakı və digər təşkilat elementlər daxil edilmir.", "placeholders": { "organization": { "content": "$1", diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 05c731aa37..bec6702ae2 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -821,7 +821,7 @@ "message": "Dieses Passwort wird zum Exportieren und Importieren dieser Datei verwendet" }, "accountRestrictedOptionDescription": { - "message": "Verwende den Verschlüsselungscode deines Kontos, abgeleitet vom Benutzernamen und Master-Passwort, um den Export zu verschlüsseln und den Import auf das aktuelle Bitwarden-Konto zu beschränken." + "message": "Verwende den Verschlüsselungsschlüssel deines Kontos, abgeleitet vom Benutzernamen und Master-Passwort, um den Export zu verschlüsseln und den Import auf das aktuelle Bitwarden-Konto zu beschränken." }, "passwordProtectedOptionDescription": { "message": "Lege ein Dateipasswort fest, um den Export zu verschlüsseln und importiere ihn in ein beliebiges Bitwarden-Konto, wobei das Passwort zum Entschlüsseln genutzt wird." diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index e44efc99da..5b426e47d1 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -423,7 +423,7 @@ "message": "Kita" }, "unlockMethods": { - "message": "Unlock options" + "message": "Atrakinti parinktis" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Nustatyk atrakinimo būdą, kad pakeistum saugyklos laiko limito veiksmą." @@ -432,10 +432,10 @@ "message": "Nustatykite nustatymuose atrakinimo metodą" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Baigėsi seanso laikas" }, "otherOptions": { - "message": "Other options" + "message": "Kitos parinktys" }, "rateExtension": { "message": "Įvertinkite šį plėtinį" @@ -2274,7 +2274,7 @@ "message": "Sugeneruoti el. pašto slapyvardį su išorine persiuntimo paslauga." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "„$SERVICENAME$“ klaida: $ERRORMESSAGE$.", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2288,11 +2288,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Sugeneravo „Bitwarden“.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Svetainė: $WEBSITE$. Sugeneravo „Bitwarden“.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2302,7 +2302,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Netinkamas „$SERVICENAME$“ API prieigos raktas.", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2312,7 +2312,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Netinkamas „$SERVICENAME$“ API prieigos raktas: $ERRORMESSAGE$.", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2326,7 +2326,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Nepavyksta gauti „$SERVICENAME$“ užmaskuoto el. pašto paskyros ID.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2336,7 +2336,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Netinkamas „$SERVICENAME$“ domenas.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2346,7 +2346,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Netinkamas „$SERVICENAME$“ URL.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2356,7 +2356,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Įvyko nežinoma „$SERVICENAME$“ klaida.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2366,7 +2366,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Nežinomas persiuntėjas: „$SERVICENAME$“.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3287,13 +3287,13 @@ "message": "Administratoriaus konsolės" }, "accountSecurity": { - "message": "Account security" + "message": "Paskyros saugumas" }, "notifications": { - "message": "Notifications" + "message": "Pranešimai" }, "appearance": { - "message": "Appearance" + "message": "Išvaizda" }, "errorAssigningTargetCollection": { "message": "Klaida priskiriant tikslinę kolekciją." diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 210f2358e0..deb7fda04b 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -763,7 +763,7 @@ "message": "Lås upp" }, "additionalOptions": { - "message": "Additional options" + "message": "Ytterligare alternativ" }, "enableContextMenuItem": { "message": "Visa alternativ för snabbmenyn" @@ -803,7 +803,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Exportera från" }, "exportVault": { "message": "Exportera valv" @@ -815,7 +815,7 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "Fillösenord" }, "exportPasswordDescription": { "message": "This password will be used to export and import this file" @@ -827,7 +827,7 @@ "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "Exporttyp" }, "accountRestricted": { "message": "Account restricted" diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 571206dbb8..a97d9626a4 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -763,7 +763,7 @@ "message": "Розблокувати" }, "additionalOptions": { - "message": "Additional options" + "message": "Додаткові налаштування" }, "enableContextMenuItem": { "message": "Показувати в контекстному меню" @@ -803,7 +803,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Експортувати з" }, "exportVault": { "message": "Експортувати сховище" @@ -812,28 +812,28 @@ "message": "Формат файлу" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Цей експортований файл буде захищений паролем, який необхідно ввести для його розшифрування." }, "filePassword": { - "message": "File password" + "message": "Пароль файлу" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Цей пароль буде використано для експортування та імпортування цього файлу" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Використовуйте ключ шифрування свого облікового запису, створений на основі імені користувача й головного пароля, щоб зашифрувати експортовані дані та обмежити можливість імпортування лише до поточного облікового запису Bitwarden." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Встановіть пароль файлу, щоб зашифрувати експортовані дані та імпортувати до будь-якого облікового запису Bitwarden за допомогою цього пароля." }, "exportTypeHeading": { - "message": "Export type" + "message": "Тип експорту" }, "accountRestricted": { - "message": "Account restricted" + "message": "Обмежено обліковим записом" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "Пароль файлу та підтвердження пароля відрізняються." }, "warning": { "message": "ПОПЕРЕДЖЕННЯ", @@ -2213,10 +2213,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Експортування сховища організації" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", "placeholders": { "organization": { "content": "$1", diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 7618173009..609275567b 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -821,10 +821,10 @@ "message": "此密码将用于导出和导入此文件" }, "accountRestrictedOptionDescription": { - "message": "使用衍生自您账户的用户名和主密码的加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" + "message": "使用衍生自您账户的用户名和主密码的账户加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" }, "passwordProtectedOptionDescription": { - "message": "设置一个密码用来加密导出的数据,并使用此密码解密以导入到任意 Bitwarden 账户。" + "message": "设置一个文件密码用来加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" }, "exportTypeHeading": { "message": "导出类型" From c72e8df619b67fd3cceb8bd37368ef3379cb2f9d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 2 Jun 2024 09:31:23 +0000 Subject: [PATCH 07/28] Autosync the updated translations (#9471) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/zh_CN/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 7196fa26e3..652a1f3d67 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -2105,7 +2105,7 @@ "message": "Bitwarden 家庭版计划。" }, "addons": { - "message": "附加项目" + "message": "插件" }, "premiumAccess": { "message": "高级会员" From 3835a9ddaf5b33ef49021dac018c47b121044847 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 2 Jun 2024 09:32:32 +0000 Subject: [PATCH 08/28] Autosync the updated translations (#9472) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/az/messages.json | 24 ++++++++++---------- apps/desktop/src/locales/de/messages.json | 2 +- apps/desktop/src/locales/sv/messages.json | 2 +- apps/desktop/src/locales/uk/messages.json | 24 ++++++++++---------- apps/desktop/src/locales/zh_CN/messages.json | 24 ++++++++++---------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index e0bcf2df5d..0143e6c274 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1300,7 +1300,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Buradan xaricə köçür" }, "exportVault": { "message": "Anbarı xaricə köçür" @@ -1309,31 +1309,31 @@ "message": "Fayl formatı" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Bu faylın xaricə köçürülməsi, parolla qorunacaq və şifrəsini açmaq üçün fayl parolu tələb olunacaq." }, "filePassword": { - "message": "File password" + "message": "Fayl parolu" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Bu parol, bu faylı daxilə və xaricə köçürmək üçün istifadə olunacaq" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Xaricə köçürməni şifrələmək və daxilə köçürməni yalnız mövcud Bitwarden hesabı ilə məhdudlaşdırmaq üçün hesabınızın istifadəçi adı və Ana Parolundan əldə edilən hesab şifrələmə açarınızı istifadə edin." }, "passwordProtected": { - "message": "Password protected" + "message": "Parolla qorunan" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Xaricə köçürməni şifrələmək üçün bir fayl parolu təyin edin və şifrəni açma parolunu istifadə edərək bunu istənilən Bitwarden hesabına köçürün." }, "exportTypeHeading": { - "message": "Export type" + "message": "Xaricə köçürmə növü" }, "accountRestricted": { - "message": "Account restricted" + "message": "Hesab məhdudlaşdırıldı" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Fayl parolu\" və \"Fayl parolunu təsdiqlə\" uyuşmur." }, "hCaptchaUrl": { "message": "hCaptcha ünvanı", @@ -2102,10 +2102,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Təşkilat anbarını xaricə köçürmə" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Yalnız $ORGANIZATION$ ilə əlaqələndirilmiş təşkilat anbarı ixrac ediləcək. Fərdi anbardakı və digər təşkilat elementlər daxil edilmir.", "placeholders": { "organization": { "content": "$1", diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 46f2e5e7f6..e5abc44372 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1318,7 +1318,7 @@ "message": "Dieses Passwort wird zum Exportieren und Importieren dieser Datei verwendet" }, "accountRestrictedOptionDescription": { - "message": "Verwende den Verschlüsselungscode deines Kontos, abgeleitet vom Benutzernamen und Master-Passwort, um den Export zu verschlüsseln und den Import auf das aktuelle Bitwarden-Konto zu beschränken." + "message": "Verwende den Verschlüsselungsschlüssel deines Kontos, abgeleitet vom Benutzernamen und Master-Passwort, um den Export zu verschlüsseln und den Import auf das aktuelle Bitwarden-Konto zu beschränken." }, "passwordProtected": { "message": "Passwortgeschützt" diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index e7dddd64aa..9bae4e883d 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1300,7 +1300,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Exportera från" }, "exportVault": { "message": "Exportera valv" diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index dc5672ed69..546005db20 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1300,7 +1300,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Експортувати з" }, "exportVault": { "message": "Експортувати сховище" @@ -1309,31 +1309,31 @@ "message": "Формат файлу" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Цей експортований файл буде захищений паролем, який необхідно ввести для його розшифрування." }, "filePassword": { - "message": "File password" + "message": "Пароль файлу" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Цей пароль буде використано для експортування та імпортування цього файлу" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Використовуйте ключ шифрування свого облікового запису, створений на основі імені користувача й головного пароля, щоб зашифрувати експортовані дані та обмежити можливість імпортування лише до поточного облікового запису Bitwarden." }, "passwordProtected": { - "message": "Password protected" + "message": "Захищено паролем" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Встановіть пароль файлу, щоб зашифрувати експортовані дані та імпортувати до будь-якого облікового запису Bitwarden за допомогою цього пароля." }, "exportTypeHeading": { - "message": "Export type" + "message": "Тип експорту" }, "accountRestricted": { - "message": "Account restricted" + "message": "Обмежено обліковим записом" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "Пароль файлу та підтвердження пароля відрізняються." }, "hCaptchaUrl": { "message": "URL-адреса hCaptcha", @@ -2102,10 +2102,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Експортування сховища організації" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", "placeholders": { "organization": { "content": "$1", diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 27686559fd..22b96d6e4b 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1300,7 +1300,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "导出自" }, "exportVault": { "message": "导出密码库" @@ -1309,31 +1309,31 @@ "message": "文件格式" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "此文件导出将受密码保护,需要文件密码才能解密。" }, "filePassword": { - "message": "File password" + "message": "文件密码" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "此密码将用于导出和导入此文件" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "使用衍生自您账户的用户名和主密码的账户加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" }, "passwordProtected": { - "message": "Password protected" + "message": "密码保护" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "设置一个文件密码用来加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" }, "exportTypeHeading": { - "message": "Export type" + "message": "导出类型" }, "accountRestricted": { - "message": "Account restricted" + "message": "账户受限" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "「文件密码」与「确认文件密码」不一致。" }, "hCaptchaUrl": { "message": "hCaptcha URL", @@ -2102,10 +2102,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "正在导出组织密码库" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库数据。不包括个人密码库和其他组织中的项目。", "placeholders": { "organization": { "content": "$1", From 2358443102666f9fa8d9b8b511cef3d90a53b054 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:52:19 -0500 Subject: [PATCH 09/28] [AC-1679] Approve all pending device authorizations (#9407) * feat: update service container for required service injection, refs AC-1679 * feat: complete approve all command, refs AC-1679 * fix: cast service container to access bit services, refs AC-1679 * fix: override service container from base program, refs AC-1679 * fix: prettier, refs AC-1679 * feat: replace hardcoded strings with i18n translations (future-proofing), refs AC-1679 * chore: remove i18n references, refs AC-1679 * fix: update approve-all and deny-all commands to match desired input, refs AC-1679 --- .../device-approval/approve-all.command.ts | 47 ++++++++++++++++++- .../device-approval.program.ts | 15 ++++-- .../bit-cli/src/service-container.ts | 19 +++++++- bitwarden_license/bit-cli/tsconfig.json | 3 +- .../src/admin-console/auth-requests/index.ts | 2 + 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts index a3a6c4943f..3214a0fc41 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts @@ -1,9 +1,52 @@ +import { firstValueFrom } from "rxjs"; + +import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; +import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; +import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; export class ApproveAllCommand { - constructor() {} + constructor( + private organizationAuthRequestService: OrganizationAuthRequestService, + private organizationService: OrganizationService, + ) {} async run(organizationId: string): Promise { - throw new Error("Not implemented"); + if (organizationId != null) { + organizationId = organizationId.toLowerCase(); + } + + if (!Utils.isGuid(organizationId)) { + return Response.badRequest("`" + organizationId + "` is not a GUID."); + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (!organization?.canManageUsersPassword) { + return Response.error( + "You do not have permission to approve pending device authorization requests.", + ); + } + + try { + const pendingApprovals = + await this.organizationAuthRequestService.listPendingRequests(organizationId); + if (pendingApprovals.length == 0) { + const res = new MessageResponse( + "No pending device authorization requests to approve.", + null, + ); + return Response.success(res); + } + + await this.organizationAuthRequestService.approvePendingRequests( + organizationId, + pendingApprovals, + ); + + return Response.success(); + } catch (e) { + return Response.error(e); + } } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts index 152dd48c7b..bf337be93d 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts @@ -3,6 +3,8 @@ import { program, Command } from "commander"; import { BaseProgram } from "@bitwarden/cli/base-program"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ServiceContainer } from "../../service-container"; + import { ApproveAllCommand } from "./approve-all.command"; import { ApproveCommand } from "./approve.command"; import { DenyAllCommand } from "./deny-all.command"; @@ -10,6 +12,10 @@ import { DenyCommand } from "./deny.command"; import { ListCommand } from "./list.command"; export class DeviceApprovalProgram extends BaseProgram { + constructor(protected serviceContainer: ServiceContainer) { + super(serviceContainer); + } + register() { program.addCommand(this.deviceApprovalCommand()); } @@ -53,14 +59,17 @@ export class DeviceApprovalProgram extends BaseProgram { } private approveAllCommand(): Command { - return new Command("approveAll") + return new Command("approve-all") .description("Approve all pending requests for an organization") .argument("") .action(async (organizationId: string) => { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ApproveAllCommand(); + const cmd = new ApproveAllCommand( + this.serviceContainer.organizationAuthRequestService, + this.serviceContainer.organizationService, + ); const response = await cmd.run(organizationId); this.processResponse(response); }); @@ -81,7 +90,7 @@ export class DeviceApprovalProgram extends BaseProgram { } private denyAllCommand(): Command { - return new Command("denyAll") + return new Command("deny-all") .description("Deny all pending requests for an organization") .argument("") .action(async (organizationId: string) => { diff --git a/bitwarden_license/bit-cli/src/service-container.ts b/bitwarden_license/bit-cli/src/service-container.ts index 369d54113d..995e14531d 100644 --- a/bitwarden_license/bit-cli/src/service-container.ts +++ b/bitwarden_license/bit-cli/src/service-container.ts @@ -1,7 +1,24 @@ +import { + OrganizationAuthRequestService, + OrganizationAuthRequestApiService, +} from "@bitwarden/bit-common/admin-console/auth-requests"; import { ServiceContainer as OssServiceContainer } from "@bitwarden/cli/service-container"; /** * Instantiates services and makes them available for dependency injection. * Any Bitwarden-licensed services should be registered here. */ -export class ServiceContainer extends OssServiceContainer {} +export class ServiceContainer extends OssServiceContainer { + organizationAuthRequestApiService: OrganizationAuthRequestApiService; + organizationAuthRequestService: OrganizationAuthRequestService; + + constructor() { + super(); + this.organizationAuthRequestApiService = new OrganizationAuthRequestApiService(this.apiService); + this.organizationAuthRequestService = new OrganizationAuthRequestService( + this.organizationAuthRequestApiService, + this.cryptoService, + this.organizationUserService, + ); + } +} diff --git a/bitwarden_license/bit-cli/tsconfig.json b/bitwarden_license/bit-cli/tsconfig.json index 1989aa08f9..e8a57e5eb0 100644 --- a/bitwarden_license/bit-cli/tsconfig.json +++ b/bitwarden_license/bit-cli/tsconfig.json @@ -21,7 +21,8 @@ "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], - "@bitwarden/node/*": ["../../libs/node/src/*"] + "@bitwarden/node/*": ["../../libs/node/src/*"], + "@bitwarden/bit-common/*": ["../../bitwarden_license/bit-common/src/*"] } }, "include": ["src", "src/**/*.spec.ts"] diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts index d8c4bacd69..517dc8699b 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts @@ -1,2 +1,4 @@ export * from "./pending-organization-auth-request.response"; export * from "./organization-auth-request.service"; +export * from "./organization-auth-request-api.service"; +export * from "./pending-auth-request.view"; From 13bccc5a63f021db3626250eba8a0ba8a4078063 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:15:01 +1000 Subject: [PATCH 10/28] [AC-2632] Device approvals ListCommand (#9389) --- .../device-approval.program.ts | 5 ++- .../device-approval/list.command.ts | 37 ++++++++++++++++++- .../pending-auth-request.response.ts | 26 +++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts index bf337be93d..290bbb6fa8 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts @@ -38,7 +38,10 @@ export class DeviceApprovalProgram extends BaseProgram { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ListCommand(); + const cmd = new ListCommand( + this.serviceContainer.organizationAuthRequestService, + this.serviceContainer.organizationService, + ); const response = await cmd.run(organizationId); this.processResponse(response); }); diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts index 11fb6ec3ee..10da11b35c 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -1,9 +1,42 @@ +import { firstValueFrom } from "rxjs"; + +import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; +import { ListResponse } from "@bitwarden/cli/models/response/list.response"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { PendingAuthRequestResponse } from "./pending-auth-request.response"; export class ListCommand { - constructor() {} + constructor( + private organizationAuthRequestService: OrganizationAuthRequestService, + private organizationService: OrganizationService, + ) {} async run(organizationId: string): Promise { - throw new Error("Not implemented"); + if (organizationId != null) { + organizationId = organizationId.toLowerCase(); + } + + if (!Utils.isGuid(organizationId)) { + return Response.badRequest("`" + organizationId + "` is not a GUID."); + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (!organization?.canManageUsersPassword) { + return Response.error( + "You do not have permission to approve pending device authorization requests.", + ); + } + + try { + const requests = + await this.organizationAuthRequestService.listPendingRequests(organizationId); + const res = new ListResponse(requests.map((r) => new PendingAuthRequestResponse(r))); + return Response.success(res); + } catch (e) { + return Response.error(e); + } } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts new file mode 100644 index 0000000000..991b3fb8e5 --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts @@ -0,0 +1,26 @@ +import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/"; +import { BaseResponse } from "@bitwarden/cli/models/response/base.response"; + +export class PendingAuthRequestResponse implements BaseResponse { + object = "auth-request"; + + id: string; + userId: string; + organizationUserId: string; + email: string; + requestDeviceIdentifier: string; + requestDeviceType: string; + requestIpAddress: string; + creationDate: Date; + + constructor(authRequest: PendingAuthRequestView) { + this.id = authRequest.id; + this.userId = authRequest.userId; + this.organizationUserId = authRequest.organizationUserId; + this.email = authRequest.email; + this.requestDeviceIdentifier = authRequest.requestDeviceIdentifier; + this.requestDeviceType = authRequest.requestDeviceType; + this.requestIpAddress = authRequest.requestIpAddress; + this.creationDate = authRequest.creationDate; + } +} From 01648e2cc39b9c2eb916dc2cb415d309cda8b309 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:15:11 -0400 Subject: [PATCH 11/28] Auth/PM-8358 - User Verification dialog & form input fix for empty submit displaying wrong error (#9363) * PM-8358 - UserVerificatonFormInput - fix incorrect init logic. We needed to execute the processSecretChanges logic to convert null into { type: 0, secret: null } (VerificationWithSecret) for all non-biometric verification flows. . * PM-8358 - UserVerificationService - verifyUser(...) - throw error if called with null. It should only happen if a dev makes a mistake in theory. --- .../user-verification-form-input.component.ts | 4 ++-- .../services/user-verification/user-verification.service.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts index 8cb40d9452..8c62136d63 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts @@ -197,8 +197,8 @@ export class UserVerificationFormInputComponent implements ControlValueAccessor, } } - // Don't bother executing secret changes if biometrics verification is active. - if (this.activeClientVerificationOption === ActiveClientVerificationOption.Biometrics) { + // Executing secret changes for all non biometrics verification. Biometrics doesn't have a user entered secret. + if (this.activeClientVerificationOption !== ActiveClientVerificationOption.Biometrics) { this.processSecretChanges(this.secret.value); } diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 7561023a27..85640519ec 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -140,6 +140,10 @@ export class UserVerificationService implements UserVerificationServiceAbstracti * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) */ async verifyUser(verification: Verification): Promise { + if (verification == null) { + throw new Error("Verification is required."); + } + const [userId, email] = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), ); From 748eb002232fce0d6959755afc2f5c6fb04e4313 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:20:14 -0500 Subject: [PATCH 12/28] [PM-6826] Vault filter refresh (#9365) * add initial type filter - use `bit-select` while the chip component is being developed * get cipherTypes$ from service - integrate with user settings * initial add of folder selection * initial add of vault selection * initial add of collections filter * update `VaultPopupListFilterService` to `VaultPopupListFiltersService` * integrate hasFilterApplied$ * intermediate commit of integration to the filters component * do not return the tree when the value is null * return null when the updated option is null * update vault-popup-list to conform to the chip select structure * integration of bit-chip-select * move "no folder" option to the end of the list * show danger icon for deactivated organizations * show deactivated warning when the filtered org is disabled * update documentation * use pascal case for constants * store filter values as full objects rather than just id - This allows secondary logic to be applied when filters are selected * move filter form into service to be the source of truth * fix tests after adding "jest-preset-angular/setup-jest" * remove logic to have dynamic cipher type filters * use ProductType enum * invert conditional for less nesting * prefer `decryptedCollections$` over getAllDecrypted * update comments * use a `filterFunction$` observable rather than having to pass filters back to the service * fix children testing * remove check for no folder * reset filter form when filter component is destroyed * add takeUntilDestroyed for organization valueChanges * allow takeUntilDestroyed to use internal destroy ref - The associated unit tests needed to be configured with TestBed rather than just `new Service()` for this to work * use controls object for type safety --- apps/browser/src/_locales/en/messages.json | 11 +- .../vault-list-filters.component.html | 39 ++ .../vault-list-filters.component.ts | 28 ++ .../components/vault/vault-v2.component.html | 18 +- .../components/vault/vault-v2.component.ts | 4 + .../vault-popup-items.service.spec.ts | 26 ++ .../services/vault-popup-items.service.ts | 48 ++- .../vault-popup-list-filters.service.spec.ts | 298 ++++++++++++++ .../vault-popup-list-filters.service.ts | 371 ++++++++++++++++++ .../src/chip-select/chip-select.component.ts | 4 +- .../src/icon/icons/deactivated-org.ts | 32 ++ libs/components/src/icon/icons/index.ts | 1 + 12 files changed, 868 insertions(+), 12 deletions(-) create mode 100644 apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html create mode 100644 apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts create mode 100644 apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts create mode 100644 apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts create mode 100644 libs/components/src/icon/icons/deactivated-org.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 426f570d64..8d81b34448 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -224,7 +224,7 @@ }, "continueToAuthenticatorPageDesc": { "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" - }, + }, "bitwardenSecretsManager": { "message": "Bitwarden Secrets Manager" }, @@ -3333,5 +3333,14 @@ "example": "Work" } } + }, + "itemsWithNoFolder": { + "message": "Items with no folder" + }, + "organizationIsDeactivated": { + "message": "Organization is deactivated" + }, + "contactYourOrgAdmin": { + "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html new file mode 100644 index 0000000000..6136db59f4 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html @@ -0,0 +1,39 @@ +
+ + + + + + + + + + + + + + +
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts new file mode 100644 index 0000000000..886e1a966a --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts @@ -0,0 +1,28 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ChipSelectComponent } from "@bitwarden/components"; + +import { VaultPopupListFiltersService } from "../../../services/vault-popup-list-filters.service"; + +@Component({ + standalone: true, + selector: "app-vault-list-filters", + templateUrl: "./vault-list-filters.component.html", + imports: [CommonModule, JslibModule, ChipSelectComponent, ReactiveFormsModule], +}) +export class VaultListFiltersComponent implements OnDestroy { + protected filterForm = this.vaultPopupListFiltersService.filterForm; + protected organizations$ = this.vaultPopupListFiltersService.organizations$; + protected collections$ = this.vaultPopupListFiltersService.collections$; + protected folders$ = this.vaultPopupListFiltersService.folders$; + protected cipherTypes = this.vaultPopupListFiltersService.cipherTypes; + + constructor(private vaultPopupListFiltersService: VaultPopupListFiltersService) {} + + ngOnDestroy(): void { + this.vaultPopupListFiltersService.resetFilterForm(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index 7d83d9f26c..4d75685f53 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -22,13 +22,13 @@ - - + +
@@ -37,7 +37,17 @@
- +
+ + {{ "organizationIsDeactivated" | i18n }} + {{ "contactYourOrgAdmin" | i18n }} + +
+ + { let service: VaultPopupItemsService; @@ -20,6 +22,8 @@ describe("VaultPopupItemsService", () => { const cipherServiceMock = mock(); const vaultSettingsServiceMock = mock(); + const organizationServiceMock = mock(); + const vaultPopupListFiltersServiceMock = mock(); const searchService = mock(); beforeEach(() => { @@ -40,6 +44,18 @@ describe("VaultPopupItemsService", () => { cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers); vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable(); vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable(); + + vaultPopupListFiltersServiceMock.filters$ = new BehaviorSubject({ + organization: null, + collection: null, + cipherType: null, + folder: null, + }); + // Return all ciphers, `filterFunction$` will be tested in `VaultPopupListFiltersService` + vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject( + (ciphers: CipherView[]) => ciphers, + ); + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); jest .spyOn(BrowserApi, "getTabFromCurrentWindow") @@ -47,6 +63,8 @@ describe("VaultPopupItemsService", () => { service = new VaultPopupItemsService( cipherServiceMock, vaultSettingsServiceMock, + vaultPopupListFiltersServiceMock, + organizationServiceMock, searchService, ); }); @@ -55,6 +73,8 @@ describe("VaultPopupItemsService", () => { service = new VaultPopupItemsService( cipherServiceMock, vaultSettingsServiceMock, + vaultPopupListFiltersServiceMock, + organizationServiceMock, searchService, ); expect(service).toBeTruthy(); @@ -87,6 +107,8 @@ describe("VaultPopupItemsService", () => { service = new VaultPopupItemsService( cipherServiceMock, vaultSettingsServiceMock, + vaultPopupListFiltersServiceMock, + organizationServiceMock, searchService, ); @@ -117,6 +139,8 @@ describe("VaultPopupItemsService", () => { service = new VaultPopupItemsService( cipherServiceMock, vaultSettingsServiceMock, + vaultPopupListFiltersServiceMock, + organizationServiceMock, searchService, ); @@ -228,6 +252,8 @@ describe("VaultPopupItemsService", () => { service = new VaultPopupItemsService( cipherServiceMock, vaultSettingsServiceMock, + vaultPopupListFiltersServiceMock, + organizationServiceMock, searchService, ); service.emptyVault$.subscribe((empty) => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 9a66ada08c..f9c37f6f7d 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -2,6 +2,8 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, combineLatest, + distinctUntilKeyChanged, + from, map, Observable, of, @@ -12,6 +14,7 @@ import { } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -20,6 +23,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; +import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; + /** * Service for managing the various item lists on the new Vault tab in the browser popup. */ @@ -72,7 +77,15 @@ export class VaultPopupItemsService { shareReplay({ refCount: false, bufferSize: 1 }), ); - private _filteredCipherList$ = combineLatest([this._cipherList$, this.searchText$]).pipe( + private _filteredCipherList$: Observable = combineLatest([ + this._cipherList$, + this.searchText$, + this.vaultPopupListFiltersService.filterFunction$, + ]).pipe( + map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [ + filterFunction(ciphers), + searchText, + ]), switchMap(([ciphers, searchText]) => this.searchService.searchCiphers(searchText, null, ciphers), ), @@ -137,10 +150,19 @@ export class VaultPopupItemsService { /** * Observable that indicates whether a filter is currently applied to the ciphers. - * @todo Implement filter/search functionality in PM-6824 and PM-6826. */ - hasFilterApplied$: Observable = this.searchText$.pipe( - switchMap((text) => this.searchService.isSearchable(text)), + hasFilterApplied$ = combineLatest([ + this.searchText$, + this.vaultPopupListFiltersService.filters$, + ]).pipe( + switchMap(([searchText, filters]) => { + return from(this.searchService.isSearchable(searchText)).pipe( + map( + (isSearchable) => + isSearchable || Object.values(filters).some((filter) => filter !== null), + ), + ); + }), ); /** @@ -156,15 +178,31 @@ export class VaultPopupItemsService { /** * Observable that indicates whether there are no ciphers to show with the current filter. - * @todo Implement filter/search functionality in PM-6824 and PM-6826. */ noFilteredResults$: Observable = this._filteredCipherList$.pipe( map((ciphers) => !ciphers.length), ); + /** Observable that indicates when the user should see the deactivated org state */ + showDeactivatedOrg$: Observable = combineLatest([ + this.vaultPopupListFiltersService.filters$.pipe(distinctUntilKeyChanged("organization")), + this.organizationService.organizations$, + ]).pipe( + map(([filters, orgs]) => { + if (!filters.organization || filters.organization.id === MY_VAULT_ID) { + return false; + } + + const org = orgs.find((o) => o.id === filters.organization.id); + return org ? !org.enabled : false; + }), + ); + constructor( private cipherService: CipherService, private vaultSettingsService: VaultSettingsService, + private vaultPopupListFiltersService: VaultPopupListFiltersService, + private organizationService: OrganizationService, private searchService: SearchService, ) {} diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts new file mode 100644 index 0000000000..eba8f94f12 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -0,0 +1,298 @@ +import { TestBed } from "@angular/core/testing"; +import { FormBuilder } from "@angular/forms"; +import { BehaviorSubject, skipWhile } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { Collection } from "@bitwarden/common/vault/models/domain/collection"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; + +import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; + +describe("VaultPopupListFiltersService", () => { + let service: VaultPopupListFiltersService; + const memberOrganizations$ = new BehaviorSubject<{ name: string; id: string }[]>([]); + const folderViews$ = new BehaviorSubject([]); + const cipherViews$ = new BehaviorSubject({}); + const decryptedCollections$ = new BehaviorSubject([]); + + const collectionService = { + decryptedCollections$, + getAllNested: () => Promise.resolve([]), + } as unknown as CollectionService; + + const folderService = { + folderViews$, + } as unknown as FolderService; + + const cipherService = { + cipherViews$, + } as unknown as CipherService; + + const organizationService = { + memberOrganizations$, + } as unknown as OrganizationService; + + const i18nService = { + t: (key: string) => key, + } as I18nService; + + beforeEach(() => { + memberOrganizations$.next([]); + decryptedCollections$.next([]); + + collectionService.getAllNested = () => Promise.resolve([]); + TestBed.configureTestingModule({ + providers: [ + { + provide: FolderService, + useValue: folderService, + }, + { + provide: CipherService, + useValue: cipherService, + }, + { + provide: OrganizationService, + useValue: organizationService, + }, + { + provide: I18nService, + useValue: i18nService, + }, + { + provide: CollectionService, + useValue: collectionService, + }, + { provide: FormBuilder, useClass: FormBuilder }, + ], + }); + + service = TestBed.inject(VaultPopupListFiltersService); + }); + + describe("cipherTypes", () => { + it("returns all cipher types", () => { + expect(service.cipherTypes.map((c) => c.value)).toEqual([ + CipherType.Login, + CipherType.Card, + CipherType.Identity, + CipherType.SecureNote, + ]); + }); + }); + + describe("organizations$", () => { + it('does not add "myVault" to the list of organizations when there are no organizations', (done) => { + memberOrganizations$.next([]); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([]); + done(); + }); + }); + + it('adds "myVault" to the list of organizations when there are other organizations', (done) => { + memberOrganizations$.next([{ name: "bobby's org", id: "1234-3323-23223" }]); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual(["myVault", "bobby's org"]); + done(); + }); + }); + + it("sorts organizations by name", (done) => { + memberOrganizations$.next([ + { name: "bobby's org", id: "1234-3323-23223" }, + { name: "alice's org", id: "2223-4343-99888" }, + ]); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([ + "myVault", + "alice's org", + "bobby's org", + ]); + done(); + }); + }); + }); + + describe("collections$", () => { + const testCollection = { + id: "14cbf8e9-7a2a-4105-9bf6-b15c01203cef", + name: "Test collection", + organizationId: "3f860945-b237-40bc-a51e-b15c01203ccf", + } as CollectionView; + + const testCollection2 = { + id: "b15c0120-7a2a-4105-9bf6-b15c01203ceg", + name: "Test collection 2", + organizationId: "1203ccf-2432-123-acdd-b15c01203ccf", + } as CollectionView; + + const testCollections = [testCollection, testCollection2]; + + beforeEach(() => { + decryptedCollections$.next(testCollections); + + collectionService.getAllNested = () => + Promise.resolve( + testCollections.map((c) => ({ + children: [], + node: c, + parent: null, + })), + ); + }); + + it("returns all collections", (done) => { + service.collections$.subscribe((collections) => { + expect(collections.map((c) => c.label)).toEqual(["Test collection", "Test collection 2"]); + done(); + }); + }); + + it("filters out collections that do not belong to an organization", () => { + service.filterForm.patchValue({ + organization: { id: testCollection2.organizationId } as Organization, + }); + + service.collections$.subscribe((collections) => { + expect(collections.map((c) => c.label)).toEqual(["Test collection 2"]); + }); + }); + }); + + describe("folders$", () => { + it('returns no folders when "No Folder" is the only option', (done) => { + folderViews$.next([{ id: null, name: "No Folder" }]); + + service.folders$.subscribe((folders) => { + expect(folders).toEqual([]); + done(); + }); + }); + + it('moves "No Folder" to the end of the list', (done) => { + folderViews$.next([ + { id: null, name: "No Folder" }, + { id: "2345", name: "Folder 2" }, + { id: "1234", name: "Folder 1" }, + ]); + + service.folders$.subscribe((folders) => { + expect(folders.map((f) => f.label)).toEqual(["Folder 1", "Folder 2", "itemsWithNoFolder"]); + done(); + }); + }); + + it("returns all folders when MyVault is selected", (done) => { + service.filterForm.patchValue({ + organization: { id: MY_VAULT_ID } as Organization, + }); + + folderViews$.next([ + { id: "1234", name: "Folder 1" }, + { id: "2345", name: "Folder 2" }, + ]); + + service.folders$.subscribe((folders) => { + expect(folders.map((f) => f.label)).toEqual(["Folder 1", "Folder 2"]); + done(); + }); + }); + + it("returns folders that have ciphers within the selected organization", (done) => { + service.folders$.pipe(skipWhile((folders) => folders.length === 2)).subscribe((folders) => { + expect(folders.map((f) => f.label)).toEqual(["Folder 1"]); + done(); + }); + + service.filterForm.patchValue({ + organization: { id: "1234" } as Organization, + }); + + folderViews$.next([ + { id: "1234", name: "Folder 1" }, + { id: "2345", name: "Folder 2" }, + ]); + + cipherViews$.next({ + "1": { folderId: "1234", organizationId: "1234" }, + "2": { folderId: "2345", organizationId: "56789" }, + }); + }); + }); + + describe("filterFunction$", () => { + const ciphers = [ + { type: CipherType.Login, collectionIds: [], organizationId: null }, + { type: CipherType.Card, collectionIds: ["1234"], organizationId: "8978" }, + { type: CipherType.Identity, collectionIds: [], folderId: "5432", organizationId: null }, + { type: CipherType.SecureNote, collectionIds: [], organizationId: null }, + ] as CipherView[]; + + it("filters by cipherType", (done) => { + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[0]]); + done(); + }); + + service.filterForm.patchValue({ cipherType: CipherType.Login }); + }); + + it("filters by collection", (done) => { + const collection = { id: "1234" } as Collection; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[1]]); + done(); + }); + + service.filterForm.patchValue({ collection }); + }); + + it("filters by folder", (done) => { + const folder = { id: "5432" } as FolderView; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[2]]); + done(); + }); + + service.filterForm.patchValue({ folder }); + }); + + describe("organizationId", () => { + it("filters out ciphers that belong to an organization when MyVault is selected", (done) => { + const organization = { id: MY_VAULT_ID } as Organization; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[0], ciphers[2], ciphers[3]]); + done(); + }); + + service.filterForm.patchValue({ organization }); + }); + + it("filters out ciphers that do not belong to the selected organization", (done) => { + const organization = { id: "8978" } as Organization; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[1]]); + done(); + }); + + service.filterForm.patchValue({ organization }); + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts new file mode 100644 index 0000000000..f3522aa8e3 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -0,0 +1,371 @@ +import { Injectable } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder } from "@angular/forms"; +import { + Observable, + combineLatest, + distinctUntilChanged, + map, + startWith, + switchMap, + tap, +} from "rxjs"; + +import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { Collection } from "@bitwarden/common/vault/models/domain/collection"; +import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { ChipSelectOption } from "@bitwarden/components"; + +/** All available cipher filters */ +export type PopupListFilter = { + organization: Organization | null; + collection: Collection | null; + folder: FolderView | null; + cipherType: CipherType | null; +}; + +/** Delimiter that denotes a level of nesting */ +const NESTING_DELIMITER = "/"; + +/** Id assigned to the "My vault" organization */ +export const MY_VAULT_ID = "MyVault"; + +const INITIAL_FILTERS: PopupListFilter = { + organization: null, + collection: null, + folder: null, + cipherType: null, +}; + +@Injectable({ + providedIn: "root", +}) +export class VaultPopupListFiltersService { + /** + * UI form for all filters + */ + filterForm = this.formBuilder.group(INITIAL_FILTERS); + + /** + * Observable for `filterForm` value + */ + filters$ = this.filterForm.valueChanges.pipe( + startWith(INITIAL_FILTERS), + ) as Observable; + + /** + * Static list of ciphers views used in synchronous context + */ + private cipherViews: CipherView[] = []; + + /** + * Observable of cipher views + */ + private cipherViews$: Observable = this.cipherService.cipherViews$.pipe( + tap((cipherViews) => { + this.cipherViews = Object.values(cipherViews); + }), + map((ciphers) => Object.values(ciphers)), + ); + + constructor( + private folderService: FolderService, + private cipherService: CipherService, + private organizationService: OrganizationService, + private i18nService: I18nService, + private collectionService: CollectionService, + private formBuilder: FormBuilder, + ) { + this.filterForm.controls.organization.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(this.validateOrganizationChange.bind(this)); + } + + /** + * Observable whose value is a function that filters an array of `CipherView` objects based on the current filters + */ + filterFunction$: Observable<(ciphers: CipherView[]) => CipherView[]> = this.filters$.pipe( + map( + (filters) => (ciphers: CipherView[]) => + ciphers.filter((cipher) => { + if (filters.cipherType !== null && cipher.type !== filters.cipherType) { + return false; + } + + if ( + filters.collection !== null && + !cipher.collectionIds.includes(filters.collection.id) + ) { + return false; + } + + if (filters.folder !== null && cipher.folderId !== filters.folder.id) { + return false; + } + + const isMyVault = filters.organization?.id === MY_VAULT_ID; + + if (isMyVault) { + if (cipher.organizationId !== null) { + return false; + } + } else if (filters.organization !== null) { + if (cipher.organizationId !== filters.organization.id) { + return false; + } + } + + return true; + }), + ), + ); + + /** + * All available cipher types + */ + readonly cipherTypes: ChipSelectOption[] = [ + { + value: CipherType.Login, + label: this.i18nService.t("logins"), + icon: "bwi-globe", + }, + { + value: CipherType.Card, + label: this.i18nService.t("cards"), + icon: "bwi-credit-card", + }, + { + value: CipherType.Identity, + label: this.i18nService.t("identities"), + icon: "bwi-id-card", + }, + { + value: CipherType.SecureNote, + label: this.i18nService.t("notes"), + icon: "bwi-sticky-note", + }, + ]; + + /** Resets `filterForm` to the original state */ + resetFilterForm(): void { + this.filterForm.reset(INITIAL_FILTERS); + } + + /** + * Organization array structured to be directly passed to `ChipSelectComponent` + */ + organizations$: Observable[]> = + this.organizationService.memberOrganizations$.pipe( + map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), + map((orgs) => { + if (!orgs.length) { + return []; + } + + return [ + // When the user is a member of an organization, make the "My Vault" option available + { + value: { id: MY_VAULT_ID } as Organization, + label: this.i18nService.t("myVault"), + icon: "bwi-user", + }, + ...orgs.map((org) => { + let icon = "bwi-business"; + + if (!org.enabled) { + // Show a warning icon if the organization is deactivated + icon = "bwi-exclamation-triangle tw-text-danger"; + } else if (org.planProductType === ProductType.Families) { + // Show a family icon if the organization is a family org + icon = "bwi-family"; + } + + return { + value: org, + label: org.name, + icon, + }; + }), + ]; + }), + ); + + /** + * Folder array structured to be directly passed to `ChipSelectComponent` + */ + folders$: Observable[]> = combineLatest([ + this.filters$.pipe( + distinctUntilChanged( + (previousFilter, currentFilter) => + // Only update the collections when the organizationId filter changes + previousFilter.organization?.id === currentFilter.organization?.id, + ), + ), + this.folderService.folderViews$, + this.cipherViews$, + ]).pipe( + map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => { + if (folders.length === 1 && folders[0].id === null) { + // Do not display folder selections when only the "no folder" option is available. + return [filters, [], cipherViews]; + } + + // Sort folders by alphabetic name + folders.sort(Utils.getSortFunction(this.i18nService, "name")); + let arrangedFolders = folders; + + const noFolder = folders.find((f) => f.id === null); + + if (noFolder) { + // Update `name` of the "no folder" option to "Items with no folder" + noFolder.name = this.i18nService.t("itemsWithNoFolder"); + + // Move the "no folder" option to the end of the list + arrangedFolders = [...folders.filter((f) => f.id !== null), noFolder]; + } + return [filters, arrangedFolders, cipherViews]; + }), + map(([filters, folders, cipherViews]) => { + const organizationId = filters.organization?.id ?? null; + + // When no org or "My vault" is selected, return all folders + if (organizationId === null || organizationId === MY_VAULT_ID) { + return folders; + } + + const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId); + + // Return only the folders that have ciphers within the filtered organization + return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id)); + }), + map((folders) => { + const nestedFolders = this.getAllFoldersNested(folders); + return new DynamicTreeNode({ + fullList: folders, + nestedList: nestedFolders, + }); + }), + map((folders) => folders.nestedList.map(this.convertToChipSelectOption.bind(this))), + ); + + /** + * Collection array structured to be directly passed to `ChipSelectComponent` + */ + collections$: Observable[]> = combineLatest([ + this.filters$.pipe( + distinctUntilChanged( + (previousFilter, currentFilter) => + // Only update the collections when the organizationId filter changes + previousFilter.organization?.id === currentFilter.organization?.id, + ), + ), + this.collectionService.decryptedCollections$, + ]).pipe( + map(([filters, allCollections]) => { + const organizationId = filters.organization?.id ?? null; + // When the organization filter is selected, filter out collections that do not belong to the selected organization + const collections = + organizationId === null + ? allCollections + : allCollections.filter((c) => c.organizationId === organizationId); + + return collections; + }), + switchMap(async (collections) => { + const nestedCollections = await this.collectionService.getAllNested(collections); + + return new DynamicTreeNode({ + fullList: collections, + nestedList: nestedCollections, + }); + }), + map((collections) => collections.nestedList.map(this.convertToChipSelectOption.bind(this))), + ); + + /** + * Converts the given item into the `ChipSelectOption` structure + */ + private convertToChipSelectOption( + item: TreeNode, + ): ChipSelectOption { + return { + value: item.node, + label: item.node.name, + icon: "bwi-folder", // Organization & Folder icons are the same + children: item.children + ? item.children.map(this.convertToChipSelectOption.bind(this)) + : undefined, + }; + } + + /** + * Returns a nested folder structure based on the input FolderView array + */ + private getAllFoldersNested(folders: FolderView[]): TreeNode[] { + const nodes: TreeNode[] = []; + + folders.forEach((f) => { + const folderCopy = new FolderView(); + folderCopy.id = f.id; + folderCopy.revisionDate = f.revisionDate; + + // Remove "/" from beginning and end of the folder name + // then split the folder name by the delimiter + const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NESTING_DELIMITER) : []; + ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NESTING_DELIMITER); + }); + + return nodes; + } + + /** + * Validate collection & folder filters when the organization filter changes + */ + private validateOrganizationChange(organization: Organization | null): void { + if (!organization) { + return; + } + + const currentFilters = this.filterForm.getRawValue(); + + // When the organization filter changes and a collection is already selected, + // reset the collection filter if the collection does not belong to the new organization filter + if (currentFilters.collection && currentFilters.collection.organizationId !== organization.id) { + this.filterForm.get("collection").setValue(null); + } + + // When the organization filter changes and a folder is already selected, + // reset the folder filter if the folder does not belong to the new organization filter + if ( + currentFilters.folder && + currentFilters.folder.id !== null && + organization.id !== MY_VAULT_ID + ) { + // Get all ciphers that belong to the new organization + const orgCiphers = this.cipherViews.filter((c) => c.organizationId === organization.id); + + // Find any ciphers within the organization that belong to the current folder + const newOrgContainsFolder = orgCiphers.some( + (oc) => oc.folderId === currentFilters.folder.id, + ); + + // If the new organization does not contain the current folder, reset the folder filter + if (!newOrgContainsFolder) { + this.filterForm.get("folder").setValue(null); + } + } + } +} diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index e91ee16f97..ff2282b72a 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -108,7 +108,7 @@ export class ChipSelectComponent implements ControlValueAccessor { */ private findOption(tree: ChipSelectOption, value: T): ChipSelectOption | null { let result = null; - if (tree.value === value) { + if (tree.value !== null && tree.value === value) { return tree; } @@ -183,7 +183,7 @@ export class ChipSelectComponent implements ControlValueAccessor { return; } - this.notifyOnChange(option?.value); + this.notifyOnChange(option?.value ?? null); } /** Implemented as part of NG_VALUE_ACCESSOR */ diff --git a/libs/components/src/icon/icons/deactivated-org.ts b/libs/components/src/icon/icons/deactivated-org.ts new file mode 100644 index 0000000000..9bb9577669 --- /dev/null +++ b/libs/components/src/icon/icons/deactivated-org.ts @@ -0,0 +1,32 @@ +import { svgIcon } from "../icon"; + +export const DeactivatedOrg = svgIcon` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/libs/components/src/icon/icons/index.ts b/libs/components/src/icon/icons/index.ts index 0e0ce4d909..9de81f1991 100644 --- a/libs/components/src/icon/icons/index.ts +++ b/libs/components/src/icon/icons/index.ts @@ -1,3 +1,4 @@ +export * from "./deactivated-org"; export * from "./search"; export * from "./no-access"; export * from "./vault"; From aee9720a6dc25482791f9310854ee289a4203792 Mon Sep 17 00:00:00 2001 From: DanHillesheim <79476558+DanHillesheim@users.noreply.github.com> Date: Mon, 3 Jun 2024 08:34:51 -0600 Subject: [PATCH 13/28] Revert "Vault - Trial initiation content updates" (#9421) --- .../content/enterprise-content.component.html | 19 ++--- .../enterprise1-content.component.html | 66 ++++++++++-------- .../enterprise2-content.component.html | 66 ++++++++++-------- .../content/teams1-content.component.html | 5 +- .../content/teams2-content.component.html | 40 ++++++++--- .../register-layout/vault-signup-badges.png | Bin 20376 -> 65051 bytes 6 files changed, 113 insertions(+), 83 deletions(-) diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html b/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html index 4abb44db4f..f57fb7a351 100644 --- a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html @@ -1,8 +1,8 @@ -

Start your 7-day free trial of Bitwarden

+

Start your 7-day Enterprise free trial

- Strengthen business security with the password manager designed for seamless administration and - employee usability. + Bitwarden is the most trusted password manager designed for seamless administration and employee + usability.

    @@ -15,14 +15,14 @@
  • Strengthen employee security practices through centralized administrative control and + >Strengthen company-wide security through centralized administrative control and policies
  • Streamline user onboarding and automate account provisioning with turnkey SSO and SCIM + >Streamline user onboarding and automate account provisioning with flexible SSO and SCIM integrations
  • @@ -35,14 +35,7 @@
  • Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Empower employees to secure their digital life at home, at work, and on the go by offering a - free Families plan to all Enterprise usersGive all Enterprise users the gift of 360º security with a free Families plan
diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html b/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html index 120748d4c0..f57fb7a351 100644 --- a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html @@ -1,34 +1,44 @@ -

The Password Manager Trusted by Millions

-
-

Everything enterprises need out of a password manager:

+

Start your 7-day Enterprise free trial

+
+

+ Bitwarden is the most trusted password manager designed for seamless administration and employee + usability. +

    -
  • Secure password sharing
  • -
  • - Easy, flexible SSO and SCIM integrations +
  • + Instantly and securely share credentials with the groups and individuals who need them +
  • +
  • + Strengthen company-wide security through centralized administrative control and + policies +
  • +
  • + Streamline user onboarding and automate account provisioning with flexible SSO and SCIM + integrations +
  • +
  • + Migrate to Bitwarden in minutes with comprehensive import options +
  • +
  • + Give all Enterprise users the gift of 360º security with a free Families plan
  • -
  • Free families plan for users
  • -
  • Quick import and migration tools
  • -
  • Simple, streamlined user experience
  • -
  • Priority support and trainers
- -
- - - -
+
diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html b/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html index 120748d4c0..f57fb7a351 100644 --- a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html @@ -1,34 +1,44 @@ -

The Password Manager Trusted by Millions

-
-

Everything enterprises need out of a password manager:

+

Start your 7-day Enterprise free trial

+
+

+ Bitwarden is the most trusted password manager designed for seamless administration and employee + usability. +

    -
  • Secure password sharing
  • -
  • - Easy, flexible SSO and SCIM integrations +
  • + Instantly and securely share credentials with the groups and individuals who need them +
  • +
  • + Strengthen company-wide security through centralized administrative control and + policies +
  • +
  • + Streamline user onboarding and automate account provisioning with flexible SSO and SCIM + integrations +
  • +
  • + Migrate to Bitwarden in minutes with comprehensive import options +
  • +
  • + Give all Enterprise users the gift of 360º security with a free Families plan
  • -
  • Free families plan for users
  • -
  • Quick import and migration tools
  • -
  • Simple, streamlined user experience
  • -
  • Priority support and trainers
- -
- - - -
+
diff --git a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html b/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html index 42f99be26b..f51c370beb 100644 --- a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html @@ -1,6 +1,5 @@ -

Start your 7-day free trial for Teams

-
-
+

Start your 7-day free trial for Teams

+

Strengthen business security with an easy-to-use password manager your team will love.

diff --git a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html b/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html index 3145e20d4f..f51c370beb 100644 --- a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html @@ -1,17 +1,35 @@ -

Start Your Free Trial Now

-
+

Start your 7-day free trial for Teams

+

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. + Strengthen business security with an easy-to-use password manager your team will love.

-