From 8589cfd96edef5b8dc38db513c402a4502f7e285 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:42:33 -0500 Subject: [PATCH 01/25] [PM-5450] Admin Console event collection (#10678) * switch `collect` to `collectMany` - The `collect` method in collection service tries to fetch the cipher via the passed `cipherId`. The cipher service fails within the admin console in some cases. * add `getCipherAdmin` call to fetch ciphers not in a collection --- .../vault/components/add-edit.component.ts | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 21a7b35ac51..960a226b1cf 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -308,9 +308,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.folders$ = this.folderService.folderViews$; if (this.editMode && this.previousCipherId !== this.cipherId) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, [this.cipher]); } this.previousCipherId = this.cipherId; this.reprompt = this.cipher.reprompt !== CipherRepromptType.None; @@ -551,12 +549,9 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.editMode && this.showPassword) { document.getElementById("loginPassword")?.focus(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledPasswordVisible, - this.cipherId, - ); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledPasswordVisible, [ + this.cipher, + ]); } } @@ -566,23 +561,18 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.editMode && this.showTotpSeed) { document.getElementById("loginTotp")?.focus(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledTOTPSeedVisible, - this.cipherId, - ); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledTOTPSeedVisible, [ + this.cipher, + ]); } } async toggleCardNumber() { this.showCardNumber = !this.showCardNumber; if (this.showCardNumber) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( + void this.eventCollectionService.collectMany( EventType.Cipher_ClientToggledCardNumberVisible, - this.cipherId, + [this.cipher], ); } } @@ -591,12 +581,9 @@ export class AddEditComponent implements OnInit, OnDestroy { this.showCardCode = !this.showCardCode; document.getElementById("cardCode").focus(); if (this.editMode && this.showCardCode) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledCardCodeVisible, - this.cipherId, - ); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientToggledCardCodeVisible, [ + this.cipher, + ]); } } @@ -742,17 +729,17 @@ export class AddEditComponent implements OnInit, OnDestroy { ); if (typeI18nKey === "password") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedPassword, [ + this.cipher, + ]); } else if (typeI18nKey === "securityCode") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedCardCode, [ + this.cipher, + ]); } else if (aType === "H_Field") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId); + void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedHiddenField, [ + this.cipher, + ]); } return true; From cc0a851c0e1462252ab462f89c9d0f5728e0783a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:29:21 -0400 Subject: [PATCH 02/25] [deps]: Lock file maintenance (#11256) * [deps]: Lock file maintenance * Lock glob version to the _very_ old one we need This seems to be due to electron-builder having old dependencies. REMOVE when electron-builder is updated --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Matt Gibson --- apps/desktop/desktop_native/Cargo.lock | 49 +- .../package-lock.json | 93 ++- package-lock.json | 574 ++++++++++++++---- package.json | 4 +- 4 files changed, 592 insertions(+), 128 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 347f8dd3f4f..c01e68f804b 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -210,9 +210,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.21" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "3bbb537bb4a30b90362caddba8f360c0a56bc13d3a5570028e7197204cb54a17" dependencies = [ "shlex", ] @@ -1250,9 +1250,12 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] [[package]] name = "ordered-stream" @@ -1357,6 +1360,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1434,18 +1443,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -1455,9 +1464,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -1466,9 +1475,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "retry" @@ -1578,9 +1587,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -1645,9 +1654,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -2175,9 +2184,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 5b44d9d6667..5dfe6e5a917 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -46,6 +46,8 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -56,17 +58,23 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -74,19 +82,27 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.10", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "license": "MIT" }, "node_modules/@types/node": { @@ -100,6 +116,8 @@ }, "node_modules/@types/node-ipc": { "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", + "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", "dev": true, "license": "MIT", "dependencies": { @@ -107,7 +125,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -117,14 +137,21 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.2", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -132,6 +159,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -145,10 +174,14 @@ }, "node_modules/arg": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -161,6 +194,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -171,14 +206,20 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/create-require": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "license": "MIT" }, "node_modules/diff": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -186,6 +227,8 @@ }, "node_modules/easy-stack": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -193,10 +236,14 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -204,6 +251,8 @@ }, "node_modules/event-pubsub": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", "license": "Unlicense", "engines": { "node": ">=4.0.0" @@ -211,6 +260,8 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -218,6 +269,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -225,6 +278,8 @@ }, "node_modules/js-message": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", "license": "MIT", "engines": { "node": ">=0.6.0" @@ -232,6 +287,8 @@ }, "node_modules/js-queue": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", "license": "MIT", "dependencies": { "easy-stack": "^1.0.1" @@ -242,14 +299,20 @@ }, "node_modules/make-error": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "license": "ISC" }, "node_modules/module-alias": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz", + "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", "license": "MIT" }, "node_modules/node-ipc": { "version": "9.2.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", + "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", "license": "MIT", "dependencies": { "event-pubsub": "4.3.0", @@ -262,6 +325,8 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -269,6 +334,8 @@ }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -281,6 +348,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -334,6 +403,8 @@ }, "node_modules/typescript": { "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -364,10 +435,14 @@ }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -383,6 +458,8 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" @@ -390,6 +467,8 @@ }, "node_modules/yargs": { "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -406,6 +485,8 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { "node": ">=12" @@ -413,6 +494,8 @@ }, "node_modules/yn": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "license": "MIT", "engines": { "node": ">=6" diff --git a/package-lock.json b/package-lock.json index c12dd4c1543..4163ead080d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -368,14 +368,14 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.5.tgz", - "integrity": "sha512-c7sVoW85Yqj7IYvNKxtNSGS5I7gWpORorg/xxLZX3OkHWXDrwYbb5LN/2p5/Aytxyb0aXl4o5fFOu6CUwcaLUw==", + "version": "0.1802.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.6.tgz", + "integrity": "sha512-oF7cPFdTLxeuvXkK/opSdIxZ1E4LrBbmuytQ/nCoAGOaKBWdqvwagRZ6jVhaI0Gwu48rkcV7Zhesg/ESNnROdw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@angular-devkit/core": "18.2.5", + "@angular-devkit/core": "18.2.6", "rxjs": "7.8.1" }, "engines": { @@ -1449,9 +1449,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.5.tgz", - "integrity": "sha512-r9TumPlJ8PvA2+yz4sp+bUHgtznaVKzhvXTN5qL1k4YP8LJ7iZWMR2FOP+HjukHZOTsenzmV9pszbogabqwoZQ==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.6.tgz", + "integrity": "sha512-la4CFvs5PcRWSkQ/H7TB5cPZirFVA9GoWk5LzIk8si6VjWBJRm8b3keKJoC9LlNeABRUIR5z0ocYkyQQUhdMfg==", "dev": true, "license": "MIT", "peer": true, @@ -5059,6 +5059,17 @@ "node": ">=10.12.0" } }, + "node_modules/@electron/asar/node_modules/@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@electron/asar/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5389,6 +5400,57 @@ "node": "*" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz", + "integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz", + "integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz", + "integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.17", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz", @@ -5406,6 +5468,312 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz", + "integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz", + "integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz", + "integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz", + "integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz", + "integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz", + "integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz", + "integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz", + "integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz", + "integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz", + "integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz", + "integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz", + "integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz", + "integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz", + "integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz", + "integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz", + "integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz", + "integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz", + "integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -7740,9 +8108,9 @@ } }, "node_modules/@storybook/angular/node_modules/@storybook/components": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.3.3.tgz", - "integrity": "sha512-i2JYtesFGkdu+Hwuj+o9fLuO3yo+LPT1/8o5xBVYtEqsgDtEAyuRUWjSz8d8NPtzloGPOv5kvR6MokWDfbeMfw==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.3.4.tgz", + "integrity": "sha512-iQzLJd87uGbFBbYNqlrN/ABrnx3dUrL0tjPCarzglzshZoPCNOsllJeJx5TJwB9kCxSZ8zB9TTOgr7NXl+oyVA==", "dev": true, "license": "MIT", "funding": { @@ -7750,13 +8118,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.3.3" + "storybook": "^8.3.4" } }, "node_modules/@storybook/angular/node_modules/@storybook/preview-api": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.3.3.tgz", - "integrity": "sha512-GP2QlaF3BBQGAyo248N7549YkTQjCentsc1hUvqPnFWU4xfjkejbnFk8yLaIw0VbYbL7jfd7npBtjZ+6AnphMQ==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.3.4.tgz", + "integrity": "sha512-/YKQ3QDVSHmtFXXCShf5w0XMlg8wkfTpdYxdGv1CKFV8DU24f3N7KWulAgeWWCWQwBzZClDa9kzxmroKlQqx3A==", "dev": true, "license": "MIT", "funding": { @@ -7764,13 +8132,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.3.3" + "storybook": "^8.3.4" } }, "node_modules/@storybook/angular/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7873,9 +8241,9 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8068,9 +8436,9 @@ } }, "node_modules/@storybook/core-webpack/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8085,9 +8453,9 @@ "license": "MIT" }, "node_modules/@storybook/core/node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "18.19.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz", + "integrity": "sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8688,9 +9056,22 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "license": "MIT", "dependencies": { @@ -8734,24 +9115,6 @@ "@types/node": "*" } }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/glob/node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -8764,9 +9127,9 @@ } }, "node_modules/@types/har-format": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.15.tgz", - "integrity": "sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", "dev": true, "license": "MIT" }, @@ -9070,6 +9433,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -9194,9 +9564,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz", - "integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==", + "version": "18.3.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz", + "integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==", "dev": true, "license": "MIT", "dependencies": { @@ -11998,9 +12368,9 @@ } }, "node_modules/autoprefixer/node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -12018,8 +12388,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -12079,9 +12449,9 @@ } }, "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", "dev": true, "license": "Apache-2.0" }, @@ -13301,9 +13671,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001663", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", - "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", + "version": "1.0.30001664", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", + "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", "funding": [ { "type": "opencollective", @@ -14654,9 +15024,9 @@ } }, "node_modules/core-js-compat/node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -14674,8 +15044,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -16241,9 +16611,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.28", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz", - "integrity": "sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==", + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", + "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==", "license": "ISC" }, "node_modules/electron-updater": { @@ -16994,9 +17364,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.1.tgz", - "integrity": "sha512-EwcbfLOhwVMAfatfqLecR2yv3dE5+kQ8kx+Rrt0DvDXEVwW86KQ/xbMDQhtp5l42VXukD5SOF8mQQHbaNtO0CQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "license": "MIT", "dependencies": { @@ -18006,9 +18376,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true, "license": "MIT" }, @@ -18580,9 +18950,9 @@ "license": "ISC" }, "node_modules/flow-parser": { - "version": "0.246.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.246.0.tgz", - "integrity": "sha512-WHRizzSrWFTcKo7cVcbP3wzZVhzsoYxoWqbnH4z+JXGqrjVmnsld6kBZWVlB200PwD5ur8r+HV3KUDxv3cHhOQ==", + "version": "0.247.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.247.1.tgz", + "integrity": "sha512-DHwcm06fWbn2Z6uFD3NaBZ5lMOoABIQ4asrVA80IWvYjjT5WdbghkUOL1wIcbLcagnFTdCZYOlSNnKNp/xnRZQ==", "dev": true, "license": "MIT", "engines": { @@ -20766,9 +21136,9 @@ } }, "node_modules/hast-util-to-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", - "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", "dev": true, "license": "MIT", "dependencies": { @@ -28879,15 +29249,15 @@ } }, "node_modules/nwsapi": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", - "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", "license": "MIT" }, "node_modules/nypm": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.11.tgz", - "integrity": "sha512-E5GqaAYSnbb6n1qZyik2wjPDZON43FqOJO59+3OkWrnmQtjggrMOVnsyzfjxp/tS6nlYJBA4zRA5jSM2YaadMg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", "dev": true, "license": "MIT", "dependencies": { @@ -29640,9 +30010,9 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, @@ -35646,9 +36016,9 @@ } }, "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.33.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.33.0.tgz", - "integrity": "sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g==", + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -37336,9 +37706,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "funding": [ { "type": "opencollective", @@ -37355,8 +37725,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/package.json b/package.json index ff7292a567e..8b8f3ed70a4 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,9 @@ "zone.js": "$zone.js" }, "replacestream": "4.0.3", - "@types/minimatch": "3.0.5" + "@electron/asar": { + "@types/glob": "7.1.3" + } }, "lint-staged": { "*": "prettier --cache --ignore-unknown --write", From 1f850363463ad7bfb730d83449817a2c250b5b60 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:13:26 +1000 Subject: [PATCH 03/25] [PM-3478] Refactor OrganizationUser api (#10949) * User and Group collection dialogs - don't fetch additional associations from the api * Refactor to use user mini-details endpoint --- .../service-container/service-container.ts | 7 +++- .../manage/entity-events.component.ts | 2 +- .../organizations/manage/events.component.ts | 4 +- .../manage/group-add-edit.component.ts | 2 +- .../collection-dialog.component.ts | 42 +++++++++++-------- .../bulk-collections-dialog.component.ts | 2 +- .../app/vault/org-vault/vault.component.ts | 14 ------- .../organization-user-api.service.ts | 15 ++++++- .../models/responses/index.ts | 1 + .../organization-user-mini.response.ts | 24 +++++++++++ .../default-organization-user-api.service.ts | 31 +++++++++++++- .../src/services/jslib-services.module.ts | 2 +- libs/common/src/enums/feature-flag.enum.ts | 2 + 13 files changed, 107 insertions(+), 41 deletions(-) create mode 100644 libs/admin-console/src/common/organization-user/models/responses/organization-user-mini.response.ts diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 6f19081a736..2149b74f614 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -498,8 +498,6 @@ export class ServiceContainer { this.providerService = new ProviderService(this.stateProvider); - this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService); - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( @@ -778,6 +776,11 @@ export class ServiceContainer { this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService); this.providerApiService = new ProviderApiService(this.apiService); + + this.organizationUserApiService = new DefaultOrganizationUserApiService( + this.apiService, + this.configService, + ); } async logout() { diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts index 79ada2b7a53..2caf2e76b72 100644 --- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts @@ -78,7 +78,7 @@ export class EntityEventsComponent implements OnInit { async load() { try { if (this.showUser) { - const response = await this.organizationUserApiService.getAllUsers( + const response = await this.organizationUserApiService.getAllMiniUserDetails( this.params.organizationId, ); response.data.forEach((u) => { diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 574335125e6..ef9d5c32d90 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -83,7 +83,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe } async load() { - const response = await this.organizationUserApiService.getAllUsers(this.organizationId); + const response = await this.organizationUserApiService.getAllMiniUserDetails( + this.organizationId, + ); response.data.forEach((u) => { const name = this.userNamePipe.transform(u); this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email }); diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 36489e0ab1d..cdbc049111d 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -131,7 +131,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { ); private get orgMembers$(): Observable> { - return from(this.organizationUserApiService.getAllUsers(this.organizationId)).pipe( + return from(this.organizationUserApiService.getAllMiniUserDetails(this.organizationId)).pipe( map((response) => response.data.map((m) => ({ id: m.id, diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 9dc8a3c0df1..5c46a7a0296 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -15,7 +15,7 @@ import { first } from "rxjs/operators"; import { OrganizationUserApiService, - OrganizationUserUserDetailsResponse, + OrganizationUserUserMiniResponse, } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -156,15 +156,23 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { organization: organization$, collections: this.collectionAdminService.getAll(orgId), groups: groups$, - // Collection(s) needed to map readonlypermission for (potential) access selector disabled state - users: this.organizationUserApiService.getAllUsers(orgId, { includeCollections: true }), + users: this.organizationUserApiService.getAllMiniUserDetails(orgId), }) .pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$)) .subscribe(({ organization, collections: allCollections, groups, users }) => { this.organization = organization; + + if (this.params.collectionId) { + this.collection = allCollections.find((c) => c.id === this.collectionId); + + if (!this.collection) { + throw new Error("Could not find collection to edit."); + } + } + this.accessItems = [].concat( - groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)), - users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)), + groups.map((group) => mapGroupToAccessItemView(group, this.collection)), + users.data.map((user) => mapUserToAccessItemView(user, this.collection)), ); // Force change detection to update the access selector's items @@ -174,15 +182,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { ? allCollections.filter((c) => c.manage) : allCollections; - if (this.params.collectionId) { - this.collection = allCollections.find((c) => c.id === this.collectionId); + if (this.collection) { // Ensure we don't allow nesting the current collection within itself this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId); - if (!this.collection) { - throw new Error("Could not find collection to edit."); - } - // Parse the name to find its parent name const { name, parent: parentName } = parseName(this.collection); @@ -423,7 +426,10 @@ function validateCanManagePermission(control: AbstractControl) { * @param collectionId Current collection being viewed/edited * @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state */ -function mapGroupToAccessItemView(group: GroupView, collectionId: string): AccessItemView { +function mapGroupToAccessItemView( + group: GroupView, + collection: CollectionAdminView, +): AccessItemView { return { id: group.id, type: AccessItemType.Group, @@ -431,8 +437,8 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces labelName: group.name, readonly: false, readonlyPermission: - collectionId != null - ? convertToPermission(group.collections.find((gc) => gc.id == collectionId)) + collection != null + ? convertToPermission(collection.groups.find((g) => g.id === group.id)) : undefined, }; } @@ -444,8 +450,8 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces * @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state */ function mapUserToAccessItemView( - user: OrganizationUserUserDetailsResponse, - collectionId: string, + user: OrganizationUserUserMiniResponse, + collection: CollectionAdminView, ): AccessItemView { return { id: user.id, @@ -457,9 +463,9 @@ function mapUserToAccessItemView( status: user.status, readonly: false, readonlyPermission: - collectionId != null + collection != null ? convertToPermission( - new CollectionAccessSelectionView(user.collections.find((uc) => uc.id == collectionId)), + new CollectionAccessSelectionView(collection.users.find((u) => u.id === user.id)), ) : undefined, }; diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index 76e90097d19..c4b0d8bc2a2 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -79,7 +79,7 @@ export class BulkCollectionsDialogComponent implements OnDestroy { combineLatest([ organization$, groups$, - this.organizationUserApiService.getAllUsers(this.params.organizationId), + this.organizationUserApiService.getAllMiniUserDetails(this.params.organizationId), ]) .pipe(takeUntil(this.destroy$)) .subscribe(([organization, groups, users]) => { diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 2976bfc8c2f..3120b54ed38 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -30,10 +30,6 @@ import { withLatestFrom, } from "rxjs/operators"; -import { - OrganizationUserApiService, - OrganizationUserUserDetailsResponse, -} from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -168,8 +164,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; - protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; - protected get hideVaultFilters(): boolean { return this.organization?.isProviderUser && !this.organization?.isMember; } @@ -206,7 +200,6 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, - private organizationUserApiService: OrganizationUserApiService, private toastService: ToastService, private accountService: AccountService, ) {} @@ -358,13 +351,6 @@ export class VaultComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - // This will be passed into the usersCanManage call - this.orgRevokedUsers = ( - await this.organizationUserApiService.getAllUsers(await firstValueFrom(organizationId$)) - ).data.filter((user: OrganizationUserUserDetailsResponse) => { - return user.status === -1; - }); - const collections$ = combineLatest([ nestedCollections$, filter$, diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts index ea5d2185ee2..ff7f9c5df6c 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts @@ -16,6 +16,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserResetPasswordDetailsResponse, OrganizationUserUserDetailsResponse, + OrganizationUserUserMiniResponse, } from "../models/responses"; /** @@ -44,7 +45,9 @@ export abstract class OrganizationUserApiService { abstract getOrganizationUserGroups(organizationId: string, id: string): Promise; /** - * Retrieve a list of all users that belong to the specified organization + * Retrieve full details of all users that belong to the specified organization. + * This is only accessible to privileged users, if you need a simple listing of basic details, use + * {@link getAllMiniUserDetails}. * @param organizationId - Identifier for the organization * @param options - Options for the request */ @@ -56,6 +59,16 @@ export abstract class OrganizationUserApiService { }, ): Promise>; + /** + * Retrieve a list of all users that belong to the specified organization, with basic information only. + * This is suitable for lists of names/emails etc. throughout the app and can be accessed by most users. + * @param organizationId - Identifier for the organization + * @param options - Options for the request + */ + abstract getAllMiniUserDetails( + organizationId: string, + ): Promise>; + /** * Retrieve reset password details for the specified organization user * @param organizationId - Identifier for the user's organization diff --git a/libs/admin-console/src/common/organization-user/models/responses/index.ts b/libs/admin-console/src/common/organization-user/models/responses/index.ts index 29c82fb18b3..aa0a968f71a 100644 --- a/libs/admin-console/src/common/organization-user/models/responses/index.ts +++ b/libs/admin-console/src/common/organization-user/models/responses/index.ts @@ -1,3 +1,4 @@ export * from "./organization-user.response"; export * from "./organization-user-bulk.response"; export * from "./organization-user-bulk-public-key.response"; +export * from "./organization-user-mini.response"; diff --git a/libs/admin-console/src/common/organization-user/models/responses/organization-user-mini.response.ts b/libs/admin-console/src/common/organization-user/models/responses/organization-user-mini.response.ts new file mode 100644 index 00000000000..6ca1bace401 --- /dev/null +++ b/libs/admin-console/src/common/organization-user/models/responses/organization-user-mini.response.ts @@ -0,0 +1,24 @@ +import { + OrganizationUserStatusType, + OrganizationUserType, +} from "@bitwarden/common/admin-console/enums"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class OrganizationUserUserMiniResponse extends BaseResponse { + id: string; + userId: string; + email: string; + name: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.userId = this.getResponseProperty("UserId"); + this.email = this.getResponseProperty("Email"); + this.name = this.getResponseProperty("Name"); + this.type = this.getResponseProperty("Type"); + this.status = this.getResponseProperty("Status"); + } +} diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts index 40824550d44..a6438b8b5ff 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts @@ -1,5 +1,9 @@ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { OrganizationUserApiService } from "../abstractions"; import { @@ -19,10 +23,14 @@ import { OrganizationUserDetailsResponse, OrganizationUserResetPasswordDetailsResponse, OrganizationUserUserDetailsResponse, + OrganizationUserUserMiniResponse, } from "../models/responses"; export class DefaultOrganizationUserApiService implements OrganizationUserApiService { - constructor(private apiService: ApiService) {} + constructor( + private apiService: ApiService, + private configService: ConfigService, + ) {} async getOrganizationUser( organizationId: string, @@ -84,6 +92,27 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer return new ListResponse(r, OrganizationUserUserDetailsResponse); } + async getAllMiniUserDetails( + organizationId: string, + ): Promise> { + const apiEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.Pm3478RefactorOrganizationUserApi), + ); + if (!apiEnabled) { + // Keep using the old api until this feature flag is enabled + return this.getAllUsers(organizationId); + } + + const r = await this.apiService.send( + "GET", + `/organizations/${organizationId}/users/mini-details`, + null, + true, + true, + ); + return new ListResponse(r, OrganizationUserUserMiniResponse); + } + async getOrganizationUserResetPasswordDetails( organizationId: string, id: string, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 1ebaf343066..0cc6e74d5b0 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -952,7 +952,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OrganizationUserApiService, useClass: DefaultOrganizationUserApiService, - deps: [ApiServiceAbstraction], + deps: [ApiServiceAbstraction, ConfigService], }), safeProvider({ provide: PasswordResetEnrollmentServiceAbstraction, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 505fe33e82a..2b7d2bea334 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -34,6 +34,7 @@ export enum FeatureFlag { AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", + Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -78,6 +79,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, + [FeatureFlag.Pm3478RefactorOrganizationUserApi]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 9a9b41a5da287ae2911614c5a866a4d248d1dcb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 23:18:05 +0200 Subject: [PATCH 04/25] [deps] Tools: Update jsdom to v25 (#10742) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 26 +++++++++++++++++++------- package.json | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index c5a28316cfd..ac0e171b94f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -66,7 +66,7 @@ "form-data": "4.0.0", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", - "jsdom": "24.1.3", + "jsdom": "25.0.1", "jszip": "3.10.1", "koa": "2.15.0", "koa-bodyparser": "4.4.1", diff --git a/package-lock.json b/package-lock.json index 4163ead080d..218e5f53a1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", - "jsdom": "24.1.3", + "jsdom": "25.0.1", "jszip": "3.10.1", "koa": "2.15.0", "koa-bodyparser": "4.4.1", @@ -209,7 +209,7 @@ "form-data": "4.0.0", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", - "jsdom": "24.1.3", + "jsdom": "25.0.1", "jszip": "3.10.1", "koa": "2.15.0", "koa-bodyparser": "4.4.1", @@ -24701,12 +24701,12 @@ } }, "node_modules/jsdom": { - "version": "24.1.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", - "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "license": "MIT", "dependencies": { - "cssstyle": "^4.0.1", + "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", @@ -24719,7 +24719,7 @@ "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.4", + "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", @@ -24765,6 +24765,18 @@ "node": ">= 14" } }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", diff --git a/package.json b/package.json index 8b8f3ed70a4..b9304f0aa87 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", - "jsdom": "24.1.3", + "jsdom": "25.0.1", "jszip": "3.10.1", "koa": "2.15.0", "koa-bodyparser": "4.4.1", From 0ae672fc0cc7a3b98f9a03973db9d5b12f9cefd9 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:46:36 -0500 Subject: [PATCH 05/25] Add missing translation strings. (#11274) --- apps/web/src/locales/en/messages.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 63dbd6ff9ce..8e847dfb63e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6591,6 +6591,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "duoHealthCheckResultsInNullAuthUrlError": { "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." }, From 5a1583cb0a42b001dfcf0803776045984d86d608 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:36:34 -0700 Subject: [PATCH 06/25] [PM-12732] - fix new send button (#11266) * fix new send button * simplify logic. use static class name where possible --- apps/browser/src/tools/popup/send-v2/send-v2.component.html | 6 +++++- .../src/new-send-dropdown/new-send-dropdown.component.html | 4 ++-- .../src/new-send-dropdown/new-send-dropdown.component.ts | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html index 23582f19115..0baa43a4b56 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -23,7 +23,11 @@ {{ "sendsNoItemsTitle" | i18n }} {{ "sendsNoItemsMessage" | i18n }} - + diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html index f1f0363c999..75334b68ef9 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html @@ -1,6 +1,6 @@ diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts index 620dc77c995..7dbe184d981 100644 --- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts +++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component, Input, OnInit } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -15,6 +15,8 @@ import { BadgeModule, ButtonModule, MenuModule } from "@bitwarden/components"; imports: [JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule, BadgeModule], }) export class NewSendDropdownComponent implements OnInit { + @Input() hideIcon: boolean = false; + sendType = SendType; hasNoPremium = false; From 3059662482652b229accf3341863a53f4b0e7dda Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:43:19 +0200 Subject: [PATCH 07/25] [deps] Tools: Update @electron/notarize to v2.5.0 (#11323) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 218e5f53a1f..d760d36e573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,7 @@ "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.25", - "@electron/notarize": "2.4.0", + "@electron/notarize": "2.5.0", "@electron/rebuild": "3.6.0", "@ngtools/webpack": "16.2.14", "@storybook/addon-a11y": "8.2.9", @@ -5194,9 +5194,9 @@ } }, "node_modules/@electron/notarize": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.4.0.tgz", - "integrity": "sha512-ArHnRPIJJGrmV+uWNQSINAht+cM4gAo3uA3WFI54bYF93mzmD15gzhPQ0Dd+v/fkMhnRiiIO8NNkGdn87Vsy0g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b9304f0aa87..b1e69fca494 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.25", - "@electron/notarize": "2.4.0", + "@electron/notarize": "2.5.0", "@electron/rebuild": "3.6.0", "@ngtools/webpack": "16.2.14", "@storybook/addon-a11y": "8.2.9", From 0846c2c822eec75eb68fa2a705fef725b25a2153 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 1 Oct 2024 13:39:36 +0200 Subject: [PATCH 08/25] [PM-11780] Resolve TypeScript 5.3 compile error --- ...rowser-api.register-content-scripts-polyfill.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts index 8a20f3e9997..b0dc60ed126 100644 --- a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts +++ b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts @@ -43,17 +43,23 @@ function buildRegisterContentScriptsPolyfill() { function NestedProxy(target: T): T { return new Proxy(target, { get(target, prop) { - if (!target[prop as keyof T]) { + const propertyValue = target[prop as keyof T]; + + if (!propertyValue) { return; } - if (typeof target[prop as keyof T] !== "function") { - return NestedProxy(target[prop as keyof T]); + if (typeof propertyValue === "object") { + return NestedProxy(propertyValue); + } + + if (typeof propertyValue !== "function") { + return propertyValue; } return (...arguments_: any[]) => new Promise((resolve, reject) => { - target[prop as keyof T](...arguments_, (result: any) => { + propertyValue(...arguments_, (result: any) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { From 7108a34ac017c0259185452c6a72535d60e4ad53 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Tue, 1 Oct 2024 07:27:07 -0500 Subject: [PATCH 09/25] [PM-12619] Passkey script cleanup process triggers breaking behavior in websites (#11304) --- .../content/fido2-page-script-append.mv2.spec.ts | 15 --------------- .../fido2/content/fido2-page-script-append.mv2.ts | 5 ----- .../content/fido2-page-script-delay-append.mv2.ts | 5 ----- 3 files changed, 25 deletions(-) diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts index f5f8dd770c7..6b9b41b5aac 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts @@ -57,19 +57,4 @@ describe("FIDO2 page-script for manifest v2", () => { ); expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); }); - - it("removes the appended `page-script.js` file after the script has triggered a load event", () => { - createdScriptElement = document.createElement("script"); - jest.spyOn(window.document, "createElement").mockImplementation((element) => { - return createdScriptElement; - }); - - require("./fido2-page-script-append.mv2"); - - jest.spyOn(createdScriptElement, "remove"); - createdScriptElement.dispatchEvent(new Event("load")); - jest.runAllTimers(); - - expect(createdScriptElement.remove).toHaveBeenCalled(); - }); }); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts index e5280c088bc..dd5f33dffb0 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts @@ -9,13 +9,8 @@ const script = globalContext.document.createElement("script"); script.src = chrome.runtime.getURL("content/fido2-page-script.js"); - script.addEventListener("load", removeScriptOnLoad); const scriptInsertionPoint = globalContext.document.head || globalContext.document.documentElement; scriptInsertionPoint.prepend(script); - - function removeScriptOnLoad() { - globalThis.setTimeout(() => script?.remove(), 5000); - } })(globalThis); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts index c75a37c1b65..2ada31fdfe2 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts @@ -9,7 +9,6 @@ const script = globalContext.document.createElement("script"); script.src = chrome.runtime.getURL("content/fido2-page-script.js"); - script.addEventListener("load", removeScriptOnLoad); // We are ensuring that the script injection is delayed in the event that we are loading // within an iframe element. This prevents an issue with web mail clients that load content @@ -29,8 +28,4 @@ globalContext.document.head || globalContext.document.documentElement; scriptInsertionPoint.prepend(script); } - - function removeScriptOnLoad() { - globalThis.setTimeout(() => script?.remove(), 5000); - } })(globalThis); From 2b78ac5151d39c5243a706c9974c40ba16077452 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:45:01 -0400 Subject: [PATCH 10/25] Show subscription status as active for premium if incomplete and within 15 seconds of creation (#11334) --- .../user-subscription.component.html | 2 +- .../individual/user-subscription.component.ts | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/billing/individual/user-subscription.component.html b/apps/web/src/app/billing/individual/user-subscription.component.html index 3430bddc134..eeb64ffe77d 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -42,7 +42,7 @@
{{ "status" | i18n }}
- {{ (subscription && subscription.status) || "-" }} + {{ (subscription && subscriptionStatus) || "-" }} {{ "pendingCancellation" | i18n }} diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index cca17f6b9cc..e04b7c8b019 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -35,8 +35,6 @@ import { UpdateLicenseDialogResult } from "../shared/update-license-types"; export class UserSubscriptionComponent implements OnInit { loading = false; firstLoaded = false; - adjustStorageAdd = true; - showUpdateLicense = false; sub: SubscriptionResponse; selfHosted = false; cloudWebVaultUrl: string; @@ -65,7 +63,7 @@ export class UserSubscriptionComponent implements OnInit { private toastService: ToastService, private configService: ConfigService, ) { - this.selfHosted = platformUtilsService.isSelfHost(); + this.selfHosted = this.platformUtilsService.isSelfHost(); } async ngOnInit() { @@ -216,11 +214,28 @@ export class UserSubscriptionComponent implements OnInit { : 0; } - get storageProgressWidth() { - return this.storagePercentage < 5 ? 5 : 0; - } - get title(): string { return this.i18nService.t(this.selfHosted ? "subscription" : "premiumMembership"); } + + get subscriptionStatus(): string | null { + if (!this.subscription) { + return null; + } else { + /* + Premium users who sign up with PayPal will have their subscription activated by a webhook. + This is an arbitrary 15-second grace period where we show their subscription as active rather than + incomplete while we wait for our webhook to process the `invoice.created` event. + */ + if (this.subscription.status === "incomplete") { + const periodStartMS = new Date(this.subscription.periodStartDate).getTime(); + const nowMS = new Date().getTime(); + return nowMS - periodStartMS <= 15000 + ? this.i18nService.t("active") + : this.subscription.status; + } + + return this.subscription.status; + } + } } From 9aeb4124048bcefe105c036bfd9d348b1a83ec99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Tue, 1 Oct 2024 16:28:56 +0200 Subject: [PATCH 11/25] [PM-7646][PM-5506] Rust IPC changes: Episode 2 (#11122) * Revert "[PM-7646][PM-5506] Revert IPC changes (#10946)" This reverts commit ed4d481e4d26d3349506407ea113da2238a4ab16. * Ensure tmp dir gets created on MacOS * Remove client reconnections * Improve client error handling and process exiting --- .github/workflows/build-desktop.yml | 45 +-- apps/desktop/desktop_native/.gitignore | 1 + apps/desktop/desktop_native/Cargo.lock | 260 +++++++++++++++++- apps/desktop/desktop_native/Cargo.toml | 2 +- apps/desktop/desktop_native/build.js | 68 +++++ apps/desktop/desktop_native/core/Cargo.toml | 37 ++- .../desktop_native/core/src/ipc/client.rs | 70 +++++ .../desktop_native/core/src/ipc/mod.rs | 66 +++++ .../desktop_native/core/src/ipc/server.rs | 232 ++++++++++++++++ apps/desktop/desktop_native/core/src/lib.rs | 6 + apps/desktop/desktop_native/napi/Cargo.toml | 6 +- apps/desktop/desktop_native/napi/build.js | 24 -- apps/desktop/desktop_native/napi/index.d.ts | 30 ++ apps/desktop/desktop_native/napi/index.js | 8 +- apps/desktop/desktop_native/napi/package.json | 4 +- apps/desktop/desktop_native/napi/src/lib.rs | 100 +++++++ apps/desktop/desktop_native/proxy/Cargo.toml | 19 ++ apps/desktop/desktop_native/proxy/src/main.rs | 159 +++++++++++ apps/desktop/electron-builder.json | 21 +- apps/desktop/package.json | 2 +- .../entitlements.desktop_proxy.plist | 12 + apps/desktop/resources/entitlements.mas.plist | 4 + .../resources/info.desktop_proxy.plist | 8 + apps/desktop/resources/native-messaging.bat | 7 - apps/desktop/scripts/after-pack.js | 138 +++++++++- apps/desktop/src/entry.ts | 42 +-- apps/desktop/src/main.ts | 15 +- .../desktop/src/main/native-messaging.main.ts | 97 ++++--- apps/desktop/src/proxy/ipc.ts | 78 ------ .../src/proxy/native-messaging-proxy.ts | 23 -- apps/desktop/src/proxy/nativemessage.ts | 95 ------- package-lock.json | 28 ++ package.json | 1 + 33 files changed, 1357 insertions(+), 351 deletions(-) create mode 100644 apps/desktop/desktop_native/build.js create mode 100644 apps/desktop/desktop_native/core/src/ipc/client.rs create mode 100644 apps/desktop/desktop_native/core/src/ipc/mod.rs create mode 100644 apps/desktop/desktop_native/core/src/ipc/server.rs delete mode 100644 apps/desktop/desktop_native/napi/build.js create mode 100644 apps/desktop/desktop_native/proxy/Cargo.toml create mode 100644 apps/desktop/desktop_native/proxy/src/main.rs create mode 100644 apps/desktop/resources/entitlements.desktop_proxy.plist create mode 100644 apps/desktop/resources/info.desktop_proxy.plist delete mode 100644 apps/desktop/resources/native-messaging.bat delete mode 100644 apps/desktop/src/proxy/ipc.ts delete mode 100644 apps/desktop/src/proxy/native-messaging-proxy.ts delete mode 100644 apps/desktop/src/proxy/nativemessage.ts diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 17748fa6a08..5022184bd05 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -174,20 +174,21 @@ jobs: with: path: | apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* ${{ env.RUNNER_TEMP }}/.cargo/registry ${{ env.RUNNER_TEMP }}/.cargo/git key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi + working-directory: apps/desktop/desktop_native env: PKG_CONFIG_ALLOW_CROSS: true PKG_CONFIG_ALL_STATIC: true TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - npm run build:cross-platform + node build.js cross-platform - name: Build application run: npm run dist:lin @@ -301,13 +302,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build & Sign (dev) env: @@ -584,13 +587,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build application (dev) run: npm run build @@ -748,13 +753,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -972,13 +979,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1205,13 +1214,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/apps/desktop/desktop_native/.gitignore b/apps/desktop/desktop_native/.gitignore index 96e7a71e1b0..1cfa7dafc20 100644 --- a/apps/desktop/desktop_native/.gitignore +++ b/apps/desktop/desktop_native/.gitignore @@ -4,3 +4,4 @@ index.node **/.DS_Store npm-debug.log* *.node +dist diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index c01e68f804b..ed6f2420b66 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.23" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbb537bb4a30b90362caddba8f360c0a56bc13d3a5570028e7197204cb54a17" +checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" dependencies = [ "shlex", ] @@ -481,6 +481,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-new" version = "0.6.0" @@ -502,10 +511,14 @@ dependencies = [ "base64", "cbc", "core-foundation", + "dirs", + "futures", "gio", + "interprocess", "keytar", "libc", "libsecret", + "log", "rand", "retry", "scopeguard", @@ -514,6 +527,7 @@ dependencies = [ "sha2", "thiserror", "tokio", + "tokio-util", "typenum", "widestring", "windows", @@ -530,6 +544,22 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "tokio", + "tokio-util", +] + +[[package]] +name = "desktop_proxy" +version = "0.0.0" +dependencies = [ + "anyhow", + "desktop_core", + "embed_plist", + "futures", + "log", + "simplelog", + "tokio", + "tokio-util", ] [[package]] @@ -542,6 +572,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dlib" version = "0.5.2" @@ -551,12 +602,24 @@ dependencies = [ "libloading", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + [[package]] name = "endi" version = "1.1.0" @@ -645,6 +708,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -652,6 +730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -719,6 +798,7 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -913,6 +993,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interprocess" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "keytar" version = "0.1.6" @@ -950,6 +1051,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "libsecret" version = "0.5.0" @@ -1038,10 +1149,21 @@ dependencies = [ ] [[package]] -name = "napi" -version = "2.16.6" +name = "mio" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc300228808a0e6aea5a58115c82889240bcf8dab16fc25ad675b33e454b368" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "napi" +version = "2.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633e41b2b983cf7983134f0c50986ca524d0caf38a2c6fc893ea3fa2e26abb0c" dependencies = [ "bitflags", "ctor", @@ -1059,9 +1181,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.5" +version = "2.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4" +checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8" dependencies = [ "cfg-if", "convert_case", @@ -1130,6 +1252,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -1140,6 +1268,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -1257,6 +1394,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-stream" version = "0.2.0" @@ -1366,6 +1509,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1441,6 +1590,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.7" @@ -1450,6 +1605,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.11.0" @@ -1631,6 +1797,17 @@ dependencies = [ "libc", ] +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1646,6 +1823,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1724,6 +1911,39 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.38.0" @@ -1732,9 +1952,13 @@ checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", + "libc", + "mio", "num_cpus", "pin-project-lite", + "socket2", "tokio-macros", + "windows-sys 0.48.0", ] [[package]] @@ -1748,6 +1972,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.19" @@ -2043,6 +2280,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index c6b77473b2a..6525b38162d 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["napi", "core"] +members = ["napi", "core", "proxy"] diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js new file mode 100644 index 00000000000..f2f012bf088 --- /dev/null +++ b/apps/desktop/desktop_native/build.js @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const child_process = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const process = require("process"); + +let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform"; + +function buildNapiModule(target, release = true) { + const targetArg = target ? `--target ${target}` : ""; + const releaseArg = release ? "--release" : ""; + return child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") }); +} + +function buildProxyBin(target, release = true) { + const targetArg = target ? `--target ${target}` : ""; + const releaseArg = release ? "--release" : ""; + return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); +} + +if (!crossPlatform) { + console.log("Building native modules in debug mode for the native architecture"); + buildNapiModule(false, false); + buildProxyBin(false, false); + return; +} + +// Note that targets contains pairs of [rust target, node arch] +// We do this to move the output binaries to a location that can +// be easily accessed from electron-builder using ${os} and ${arch} +let targets = []; +switch (process.platform) { + case "win32": + targets = [ + ["i686-pc-windows-msvc", 'ia32'], + ["x86_64-pc-windows-msvc", 'x64'], + ["aarch64-pc-windows-msvc", 'arm64'] + ]; + break; + + case "darwin": + targets = [ + ["x86_64-apple-darwin", 'x64'], + ["aarch64-apple-darwin", 'arm64'] + ]; + break; + + default: + targets = [ + ['x86_64-unknown-linux-musl', 'x64'] + ]; + + process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; + process.env["PKG_CONFIG_ALL_STATIC"] = "1"; + break; +} + +console.log("Cross building native modules for the targets: ", targets.map(([target, _]) => target).join(", ")); + +fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true }); + +targets.forEach(([target, nodeArch]) => { + buildNapiModule(target); + buildProxyBin(target); + + const ext = process.platform === "win32" ? ".exe" : ""; + fs.copyFileSync(path.join(__dirname, "target", target, "release", `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`)); +}); diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 108d6124dae..03f0ef6d696 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -6,9 +6,21 @@ version = "0.0.0" publish = false [features] -default = [] +default = ["sys"] manual_test = [] +sys = [ + "dep:widestring", + "dep:windows", + "dep:core-foundation", + "dep:security-framework", + "dep:security-framework-sys", + "dep:gio", + "dep:libsecret", + "dep:zbus", + "dep:zbus_polkit", +] + [dependencies] aes = "=0.8.4" anyhow = "=1.0.86" @@ -17,17 +29,22 @@ arboard = { version = "=3.4.1", default-features = false, features = [ ] } base64 = "=0.22.1" cbc = { version = "=0.1.2", features = ["alloc"] } +dirs = "=5.0.1" +futures = "=0.3.30" +interprocess = { version = "=2.2.1", features = ["tokio"] } libc = "=0.2.155" +log = "=0.4.22" rand = "=0.8.5" retry = "=2.0.0" scopeguard = "=1.2.0" sha2 = "=0.10.8" thiserror = "=1.0.61" tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] } +tokio-util = "=0.7.11" typenum = "=1.17.0" [target.'cfg(windows)'.dependencies] -widestring = "=1.1.0" +widestring = { version = "=1.1.0", optional = true } windows = { version = "=0.57.0", features = [ "Foundation", "Security_Credentials_UI", @@ -38,18 +55,18 @@ windows = { version = "=0.57.0", features = [ "Win32_System_WinRT", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", -] } +], optional = true } [target.'cfg(windows)'.dev-dependencies] keytar = "=0.1.6" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "=0.9.4" -security-framework = "=2.11.0" -security-framework-sys = "=2.11.0" +core-foundation = { version = "=0.9.4", optional = true } +security-framework = { version = "=2.11.0", optional = true } +security-framework-sys = { version = "=2.11.0", optional = true } [target.'cfg(target_os = "linux")'.dependencies] -gio = "=0.19.5" -libsecret = "=0.5.0" -zbus = "=4.3.1" -zbus_polkit = "=4.0.0" +gio = { version = "=0.19.5", optional = true } +libsecret = { version = "=0.5.0", optional = true } +zbus = { version = "=4.3.1", optional = true } +zbus_polkit = { version = "=4.0.0", optional = true } diff --git a/apps/desktop/desktop_native/core/src/ipc/client.rs b/apps/desktop/desktop_native/core/src/ipc/client.rs new file mode 100644 index 00000000000..7eff8a10974 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/client.rs @@ -0,0 +1,70 @@ +use std::path::PathBuf; + +use interprocess::local_socket::{ + tokio::{prelude::*, Stream}, + GenericFilePath, ToFsName, +}; +use log::{error, info}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::ipc::NATIVE_MESSAGING_BUFFER_SIZE; + +pub async fn connect( + path: PathBuf, + send: tokio::sync::mpsc::Sender, + mut recv: tokio::sync::mpsc::Receiver, +) -> Result<(), Box> { + info!("Attempting to connect to {}", path.display()); + + let name = path.as_os_str().to_fs_name::()?; + let mut conn = Stream::connect(name).await?; + + info!("Connected to {}", path.display()); + + // This `connected` and the latter `disconnected` messages are the only ones that + // are sent from the Rust IPC code and not just forwarded from the desktop app. + // As it's only two, we hardcode the JSON values to avoid pulling in a JSON library. + send.send("{\"command\":\"connected\"}".to_owned()).await?; + + let mut buffer = vec![0; NATIVE_MESSAGING_BUFFER_SIZE]; + + // Listen to IPC messages + loop { + tokio::select! { + // Forward messages to the IPC server + msg = recv.recv() => { + match msg { + Some(msg) => { + conn.write_all(msg.as_bytes()).await?; + } + None => { + info!("Client channel closed"); + break; + }, + } + }, + + // Forward messages from the IPC server + res = conn.read(&mut buffer[..]) => { + match res { + Err(e) => { + error!("Error reading from IPC server: {e}"); + break; + } + Ok(0) => { + info!("Connection closed"); + break; + } + Ok(n) => { + let message = String::from_utf8_lossy(&buffer[..n]).to_string(); + send.send(message).await?; + } + } + } + } + } + + let _ = send.send("{\"command\":\"disconnected\"}".to_owned()).await; + + Ok(()) +} diff --git a/apps/desktop/desktop_native/core/src/ipc/mod.rs b/apps/desktop/desktop_native/core/src/ipc/mod.rs new file mode 100644 index 00000000000..c7ac1a43404 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/mod.rs @@ -0,0 +1,66 @@ +pub mod client; +pub mod server; + +/// The maximum size of a message that can be sent over IPC. +/// According to the documentation, the maximum size sent to the browser is 1MB. +/// While the maximum size sent from the browser to the native messaging host is 4GB. +/// +/// Currently we are setting the maximum both ways to be 1MB. +/// +/// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side +/// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol +pub const NATIVE_MESSAGING_BUFFER_SIZE: usize = 1024 * 1024; + +/// The maximum number of messages that can be buffered in a channel. +/// This number is more or less arbitrary and can be adjusted as needed, +/// but ideally the messages should be processed as quickly as possible. +pub const MESSAGE_CHANNEL_BUFFER: usize = 32; + +/// Resolve the path to the IPC socket. +pub fn path(name: &str) -> std::path::PathBuf { + #[cfg(target_os = "windows")] + { + // Use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user. + // Hashing prevents problems with reserved characters and file length limitations. + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + use sha2::Digest; + let home = dirs::home_dir().unwrap(); + let hash = sha2::Sha256::digest(home.as_os_str().as_encoded_bytes()); + let hash_b64 = URL_SAFE_NO_PAD.encode(hash.as_slice()); + + format!(r"\\.\pipe\{hash_b64}.app.{name}").into() + } + + #[cfg(target_os = "macos")] + { + let mut home = dirs::home_dir().unwrap(); + + // When running in an unsandboxed environment, path is: /Users// + // While running sandboxed, it's different: /Users//Library/Containers/com.bitwarden.desktop/Data + // + // We want to use App Groups in /Users//Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop, + // so we need to remove all the components after the user. + // Note that we subtract 3 because the root directory is counted as a component (/, Users, ). + let num_components = home.components().count(); + for _ in 0..num_components - 3 { + home.pop(); + } + + let tmp = home.join("Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp"); + + // The tmp directory might not exist, so create it + let _ = std::fs::create_dir_all(&tmp); + tmp.join(format!("app.{name}")) + } + + #[cfg(target_os = "linux")] + { + // On Linux, we use the user's cache directory. + let home = dirs::cache_dir().unwrap(); + let path_dir = home.join("com.bitwarden.desktop"); + + // The chache directory might not exist, so create it + let _ = std::fs::create_dir_all(&path_dir); + path_dir.join(format!("app.{name}")) + } +} diff --git a/apps/desktop/desktop_native/core/src/ipc/server.rs b/apps/desktop/desktop_native/core/src/ipc/server.rs new file mode 100644 index 00000000000..053b4322203 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/server.rs @@ -0,0 +1,232 @@ +use std::{error::Error, path::Path, vec}; + +use futures::TryFutureExt; + +use anyhow::Result; +use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions}; +use log::{error, info}; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + sync::{broadcast, mpsc}, +}; +use tokio_util::sync::CancellationToken; + +use super::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE}; + +#[derive(Debug)] +pub struct Message { + pub client_id: u32, + pub kind: MessageType, + // This value should be Some for MessageType::Message and None for the rest + pub message: Option, +} + +#[derive(Debug)] +pub enum MessageType { + Connected, + Disconnected, + Message, +} + +pub struct Server { + cancel_token: CancellationToken, + server_to_clients_send: broadcast::Sender, +} + +impl Server { + /// Create and start the IPC server without blocking. + /// + /// # Parameters + /// + /// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + /// - `client_to_server_send`: This [`mpsc::Sender`] will receive all the [`Message`]'s that the clients send to this server. + pub fn start( + path: &Path, + client_to_server_send: mpsc::Sender, + ) -> Result> { + // If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first. + // Any processes that were using the old socket should remain connected to it but any new connections will use the new socket. + if !cfg!(windows) { + let _ = std::fs::remove_file(path); + } + + let name = path.as_os_str().to_fs_name::()?; + let opts = ListenerOptions::new().name(name); + let listener = opts.create_tokio()?; + + // This broadcast channel is used for sending messages to all connected clients, and so the sender + // will be stored in the server while the receiver will be cloned and passed to each client handler. + let (server_to_clients_send, server_to_clients_recv) = + broadcast::channel::(MESSAGE_CHANNEL_BUFFER); + + // This cancellation token allows us to cleanly stop the server and all the spawned + // tasks without having to wait on all the pending tasks finalizing first + let cancel_token = CancellationToken::new(); + + // Create the server and start listening for incoming connections + // in a separate task to avoid blocking the current task + let server = Server { + cancel_token: cancel_token.clone(), + server_to_clients_send, + }; + tokio::spawn(listen_incoming( + listener, + client_to_server_send, + server_to_clients_recv, + cancel_token, + )); + + Ok(server) + } + + /// Send a message over the IPC server to all the connected clients + /// + /// # Returns + /// + /// The number of clients that the message was sent to. Note that the number of messages + /// sent may be less than the number of connected clients if some clients disconnect while + /// the message is being sent. + pub fn send(&self, message: String) -> Result { + let sent = self.server_to_clients_send.send(message)?; + Ok(sent) + } + + /// Stop the IPC server. + pub fn stop(&self) { + self.cancel_token.cancel(); + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.stop(); + } +} + +async fn listen_incoming( + listener: LocalSocketListener, + client_to_server_send: mpsc::Sender, + server_to_clients_recv: broadcast::Receiver, + cancel_token: CancellationToken, +) { + // We use a simple incrementing ID for each client + let mut next_client_id = 1_u32; + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("IPC server cancelled."); + break; + }, + + // A new client connection has been established + msg = listener.accept() => { + match msg { + Ok(client_stream) => { + let client_id = next_client_id; + next_client_id += 1; + + let future = handle_connection( + client_stream, + client_to_server_send.clone(), + // We resubscribe to the receiver here so this task can have it's own copy + // Note that this copy will only receive messages sent after this point, + // but that is okay, realistically we don't want any messages before we get a chance + // to send the connected message to the client, which is done inside [`handle_connection`] + server_to_clients_recv.resubscribe(), + cancel_token.clone(), + client_id + ); + tokio::spawn(future.map_err(|e| { + error!("Error handling connection: {}", e) + })); + }, + Err(e) => { + error!("Error accepting connection: {}", e); + break; + }, + } + } + } + } +} + +async fn handle_connection( + mut client_stream: impl AsyncRead + AsyncWrite + Unpin, + client_to_server_send: mpsc::Sender, + mut server_to_clients_recv: broadcast::Receiver, + cancel_token: CancellationToken, + client_id: u32, +) -> Result<(), Box> { + client_to_server_send + .send(Message { + client_id, + kind: MessageType::Connected, + message: None, + }) + .await?; + + let mut buf = vec![0u8; NATIVE_MESSAGING_BUFFER_SIZE]; + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("Client {client_id} cancelled."); + break; + }, + + // Forward messages to the IPC clients + msg = server_to_clients_recv.recv() => { + match msg { + Ok(msg) => { + client_stream.write_all(msg.as_bytes()).await?; + }, + Err(e) => { + info!("Error reading message: {}", e); + break; + } + } + }, + + // Forwards messages from the IPC clients to the server + // Note that we also send connect and disconnect events so that + // the server can keep track of multiple clients + result = client_stream.read(&mut buf) => { + match result { + Err(e) => { + info!("Error reading from client {client_id}: {e}"); + + client_to_server_send.send(Message { + client_id, + kind: MessageType::Disconnected, + message: None, + }).await?; + break; + }, + Ok(0) => { + info!("Client {client_id} disconnected."); + + client_to_server_send.send(Message { + client_id, + kind: MessageType::Disconnected, + message: None, + }).await?; + break; + }, + Ok(size) => { + let msg = std::str::from_utf8(&buf[..size])?; + + client_to_server_send.send(Message { + client_id, + kind: MessageType::Message, + message: Some(msg.to_string()), + }).await?; + }, + + } + } + } + } + + Ok(()) +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index d23a285b4ac..3132c56f7f8 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "sys")] pub mod biometric; +#[cfg(feature = "sys")] pub mod clipboard; pub mod crypto; pub mod error; +pub mod ipc; +#[cfg(feature = "sys")] pub mod password; +#[cfg(feature = "sys")] pub mod process_isolation; +#[cfg(feature = "sys")] pub mod powermonitor; diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 942ccdba212..6fb710b0671 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -16,8 +16,10 @@ manual_test = [] [dependencies] anyhow = "=1.0.86" desktop_core = { path = "../core" } -napi = { version = "=2.16.6", features = ["async"] } -napi-derive = "=2.16.5" +napi = { version = "=2.16.7", features = ["async"] } +napi-derive = "=2.16.6" +tokio = { version = "1.38.0" } +tokio-util = "0.7.11" [build-dependencies] napi-build = "=2.1.3" diff --git a/apps/desktop/desktop_native/napi/build.js b/apps/desktop/desktop_native/napi/build.js deleted file mode 100644 index 6c92dbad1b6..00000000000 --- a/apps/desktop/desktop_native/napi/build.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const child_process = require("child_process"); -const process = require("process"); - -let targets = []; -switch (process.platform) { - case "win32": - targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]; - break; - - case "darwin": - targets = ["x86_64-apple-darwin", "aarch64-apple-darwin"]; - break; - - default: - targets = ['x86_64-unknown-linux-musl']; - process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; - process.env["PKG_CONFIG_ALL_STATIC"] = "1"; - break; -} - -targets.forEach(target => { - child_process.execSync(`npm run build -- --target ${target}`, {stdio: 'inherit'}); -}); diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index deaf6b8e57f..fe4ab59fd8e 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -51,3 +51,33 @@ export namespace powermonitors { export function onLock(callback: (err: Error | null, ) => any): Promise export function isLockMonitorAvailable(): Promise } +export namespace ipc { + export interface IpcMessage { + clientId: number + kind: IpcMessageType + message?: string + } + export const enum IpcMessageType { + Connected = 0, + Disconnected = 1, + Message = 2 + } + export class IpcServer { + /** + * Create and start the IPC server without blocking. + * + * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + * @param callback This function will be called whenever a message is received from a client. + */ + static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise + /** Stop the IPC server. */ + stop(): void + /** + * Send a message over the IPC server to all the connected clients + * + * @return The number of clients that the message was sent to. Note that the number of messages + * actually received may be less, as some clients could disconnect before receiving the message. + */ + send(message: string): number + } +} diff --git a/apps/desktop/desktop_native/napi/index.js b/apps/desktop/desktop_native/napi/index.js index 680f1302b9a..a0cfee8e1a0 100644 --- a/apps/desktop/desktop_native/napi/index.js +++ b/apps/desktop/desktop_native/napi/index.js @@ -206,10 +206,4 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { passwords, biometrics, clipboards, processisolations, powermonitors } = nativeBinding - -module.exports.passwords = passwords -module.exports.biometrics = biometrics -module.exports.clipboards = clipboards -module.exports.processisolations = processisolations -module.exports.powermonitors = powermonitors +module.exports = nativeBinding diff --git a/apps/desktop/desktop_native/napi/package.json b/apps/desktop/desktop_native/napi/package.json index 70e472b3952..9f098c4965d 100644 --- a/apps/desktop/desktop_native/napi/package.json +++ b/apps/desktop/desktop_native/napi/package.json @@ -3,9 +3,7 @@ "version": "0.1.0", "description": "", "scripts": { - "build": "napi build --release --platform --js false", - "build:debug": "napi build --platform --js false", - "build:cross-platform": "node build.js", + "build": "napi build --platform --js false", "test": "cargo test" }, "author": "", diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index dfdc316d259..838eb651244 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -189,3 +189,103 @@ pub mod powermonitors { } } + +#[napi] +pub mod ipc { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ + ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + + #[napi(object)] + pub struct IpcMessage { + pub client_id: u32, + pub kind: IpcMessageType, + pub message: Option, + } + + impl From for IpcMessage { + fn from(message: Message) -> Self { + IpcMessage { + client_id: message.client_id, + kind: message.kind.into(), + message: message.message, + } + } + } + + #[napi] + pub enum IpcMessageType { + Connected, + Disconnected, + Message, + } + + impl From for IpcMessageType { + fn from(message_type: MessageType) -> Self { + match message_type { + MessageType::Connected => IpcMessageType::Connected, + MessageType::Disconnected => IpcMessageType::Disconnected, + MessageType::Message => IpcMessageType::Message, + } + } + } + + #[napi] + pub struct IpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl IpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + /// @param callback This function will be called whenever a message is received from a client. + #[napi(factory)] + pub async fn listen( + name: String, + #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] + callback: ThreadsafeFunction, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(message) = recv.recv().await { + callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(IpcServer { server }) + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + /// Send a message over the IPC server to all the connected clients + /// + /// @return The number of clients that the message was sent to. Note that the number of messages + /// actually received may be less, as some clients could disconnect before receiving the message. + #[napi] + pub fn send(&self, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml new file mode 100644 index 00000000000..681c34c8eab --- /dev/null +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2021" +exclude = ["index.node"] +license = "GPL-3.0" +name = "desktop_proxy" +version = "0.0.0" +publish = false + +[dependencies] +anyhow = "=1.0.86" +desktop_core = { path = "../core", default-features = false } +futures = "0.3.30" +log = "0.4.21" +simplelog = "0.12.2" +tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "rt"] } +tokio-util = { version = "0.7.11", features = ["codec"] } + +[target.'cfg(target_os = "macos")'.dependencies] +embed_plist = "1.2.2" diff --git a/apps/desktop/desktop_native/proxy/src/main.rs b/apps/desktop/desktop_native/proxy/src/main.rs new file mode 100644 index 00000000000..7d3b4ecfca7 --- /dev/null +++ b/apps/desktop/desktop_native/proxy/src/main.rs @@ -0,0 +1,159 @@ +use std::path::Path; + +use desktop_core::ipc::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE}; +use futures::{FutureExt, SinkExt, StreamExt}; +use log::*; +use tokio_util::codec::LengthDelimitedCodec; + +#[cfg(target_os = "macos")] +embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist"); + +fn init_logging(log_path: &Path, level: log::LevelFilter) { + use simplelog::{ColorChoice, CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode}; + + let config = Config::default(); + + let mut loggers: Vec> = Vec::new(); + loggers.push(TermLogger::new( + level, + config.clone(), + TerminalMode::Stderr, + ColorChoice::Auto, + )); + + match std::fs::File::create(log_path) { + Ok(file) => { + loggers.push(simplelog::WriteLogger::new(level, config, file)); + } + Err(e) => { + eprintln!("Can't create file: {}", e); + } + } + + if let Err(e) = CombinedLogger::init(loggers) { + eprintln!("Failed to initialize logger: {}", e); + } +} + +/// Bitwarden IPC Proxy. +/// +/// This proxy allows browser extensions to communicate with a desktop application using Native +/// Messaging. This method allows an extension to send and receive messages through the use of +/// stdin/stdout streams. +/// +/// However, this also requires the browser to start the process in order for the communication to +/// occur. To overcome this limitation, we implement Inter-Process Communication (IPC) to establish +/// a stable communication channel between the proxy and the running desktop application. +/// +/// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop +/// +#[tokio::main(flavor = "current_thread")] +async fn main() { + let sock_path = desktop_core::ipc::path("bitwarden"); + + let log_path = { + let mut path = sock_path.clone(); + path.set_extension("bitwarden.log"); + path + }; + + init_logging(&log_path, LevelFilter::Info); + + info!("Starting Bitwarden IPC Proxy."); + + // Different browsers send different arguments when the app starts: + // + // Firefox: + // - The complete path to the app manifest. (in the form `/Users//Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`) + // - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in the form `{[UUID]}`). + // + // Chrome on Windows: + // - Origin of the extension that started it (in the form `chrome-extension://[ID]`). + // - Handle to the Chrome native window that started the app. + // + // Chrome on Linux and Mac: + // - Origin of the extension that started it (in the form `chrome-extension://[ID]`). + + let args: Vec<_> = std::env::args().skip(1).collect(); + info!("Process args: {:?}", args); + + // Setup two channels, one for sending messages to the desktop application (`out`) and one for receiving messages from the desktop application (`in`) + let (in_send, in_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER); + let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER); + + let mut handle = tokio::spawn( + desktop_core::ipc::client::connect(sock_path, out_send, in_recv) + .map(|r| r.map_err(|e| e.to_string())), + ); + + // Create a new codec for reading and writing messages from stdin/stdout. + let mut stdin = LengthDelimitedCodec::builder() + .max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE) + .native_endian() + .new_read(tokio::io::stdin()); + let mut stdout = LengthDelimitedCodec::builder() + .max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE) + .native_endian() + .new_write(tokio::io::stdout()); + + loop { + tokio::select! { + // This forces tokio to poll the futures in the order that they are written. + // We want the spawn handle to be evaluated first so that we can get any error + // results before we get the channel closed message. + biased; + + // IPC client has finished, so we should exit as well. + res = &mut handle => { + match res { + Ok(Ok(())) => { + info!("IPC client finished successfully."); + std::process::exit(0); + } + Ok(Err(e)) => { + error!("IPC client connection error: {}", e); + std::process::exit(1); + } + Err(e) => { + error!("IPC client spawn error: {}", e); + std::process::exit(1); + } + } + } + + // Receive messages from IPC and print to STDOUT. + msg = out_recv.recv() => { + match msg { + Some(msg) => { + debug!("OUT: {}", msg); + stdout.send(msg.into()).await.unwrap(); + } + None => { + info!("Channel closed, exiting."); + std::process::exit(0); + } + } + }, + + // Listen to stdin and send messages to ipc processor. + msg = stdin.next() => { + match msg { + Some(Ok(msg)) => { + let m = String::from_utf8(msg.to_vec()).unwrap(); + debug!("IN: {}", m); + in_send.send(m).await.unwrap(); + } + Some(Err(e)) => { + error!("Error parsing input: {}", e); + std::process::exit(1); + } + None => { + info!("Received EOF, exiting."); + std::process::exit(0); + } + } + } + + } + } +} diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 09783f26f49..be30e063c1a 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -73,6 +73,13 @@ "CFBundleDevelopmentRegion": "en" }, "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", + "extraFiles": [ + { + "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}", + "to": "MacOS/desktop_proxy" + } + ], + "signIgnore": ["MacOS/desktop_proxy"], "target": ["dmg", "zip"] }, "win": { @@ -84,16 +91,24 @@ "from": "../../node_modules/regedit/vbs", "to": "regedit/vbs", "filter": ["**/*"] - }, + } + ], + "extraFiles": [ { - "from": "resources/native-messaging.bat", - "to": "native-messaging.bat" + "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", + "to": "desktop_proxy.exe" } ] }, "linux": { "category": "Utility", "synopsis": "A secure and free password manager for all of your devices.", + "extraFiles": [ + { + "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}", + "to": "desktop_proxy" + } + ], "target": ["deb", "freebsd", "rpm", "AppImage", "snap"], "desktop": { "Name": "Bitwarden", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c50e7ccbac4..fbf598327f4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,7 +18,7 @@ "scripts": { "postinstall": "electron-rebuild", "start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", - "build-native": "cd desktop_native/napi && npm run build", + "build-native": "cd desktop_native && node build.js", "build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", diff --git a/apps/desktop/resources/entitlements.desktop_proxy.plist b/apps/desktop/resources/entitlements.desktop_proxy.plist new file mode 100644 index 00000000000..d5c7b8a2cc8 --- /dev/null +++ b/apps/desktop/resources/entitlements.desktop_proxy.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + LTZ2PFU5D6.com.bitwarden.desktop + + + diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index 5bfeba83a61..d42ade962c3 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -8,6 +8,10 @@ LTZ2PFU5D6 com.apple.security.app-sandbox + com.apple.security.application-groups + + LTZ2PFU5D6.com.bitwarden.desktop + com.apple.security.network.client com.apple.security.files.user-selected.read-write diff --git a/apps/desktop/resources/info.desktop_proxy.plist b/apps/desktop/resources/info.desktop_proxy.plist new file mode 100644 index 00000000000..d3c30e3e0eb --- /dev/null +++ b/apps/desktop/resources/info.desktop_proxy.plist @@ -0,0 +1,8 @@ + + + + + CFBundleIdentifier + com.bitwarden.desktop + + diff --git a/apps/desktop/resources/native-messaging.bat b/apps/desktop/resources/native-messaging.bat deleted file mode 100644 index 45519250dd6..00000000000 --- a/apps/desktop/resources/native-messaging.bat +++ /dev/null @@ -1,7 +0,0 @@ -@echo off -:: Helper script for starting the Native Messaging Proxy on Windows. - -cd ../ -set ELECTRON_RUN_AS_NODE=1 -set ELECTRON_NO_ATTACH_CONSOLE=1 -Bitwarden.exe resources/app.asar %* diff --git a/apps/desktop/scripts/after-pack.js b/apps/desktop/scripts/after-pack.js index e128397e615..08cff76e858 100644 --- a/apps/desktop/scripts/after-pack.js +++ b/apps/desktop/scripts/after-pack.js @@ -1,14 +1,22 @@ /* eslint-disable @typescript-eslint/no-var-requires, no-console */ require("dotenv").config(); +const child_process = require("child_process"); const path = require("path"); +const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses"); +const builder = require("electron-builder"); const fse = require("fs-extra"); exports.default = run; async function run(context) { console.log("## After pack"); - console.log(context); + // console.log(context); + + if (context.packager.platform.nodeName !== "darwin" || context.arch === builder.Arch.universal) { + await addElectronFuses(context); + } + if (context.electronPlatformName === "linux") { console.log("Creating memory-protection wrapper script"); const appOutDir = context.appOutDir; @@ -23,4 +31,132 @@ async function run(context) { fse.chmodSync(wrapperBin, "755"); console.log("Copied memory-protection wrapper script"); } + + if (["darwin", "mas"].includes(context.electronPlatformName)) { + const is_mas = context.electronPlatformName === "mas"; + const is_mas_dev = context.targets.some((e) => e.name === "mas-dev"); + + let id; + + // Only use the Bitwarden Identities on CI + if (process.env.GITHUB_ACTIONS === "true") { + if (is_mas) { + id = is_mas_dev + ? "E7C9978F6FBCE0553429185C405E61F5380BE8EB" + : "3rd Party Mac Developer Application: Bitwarden Inc"; + } else { + id = "Developer ID Application: 8bit Solutions LLC"; + } + // Locally, use the first valid code signing identity, unless CSC_NAME is set + } else if (process.env.CSC_NAME) { + id = process.env.CSC_NAME; + } else { + const identities = getIdentities(); + if (identities.length === 0) { + throw new Error("No valid identities found"); + } + id = identities[0].id; + } + + console.log(`Signing proxy binary before the main bundle, using identity '${id}'`); + + const appName = context.packager.appInfo.productFilename; + const appPath = `${context.appOutDir}/${appName}.app`; + const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy"); + + const packageId = "com.bitwarden.desktop"; + const entitlementsName = "entitlements.desktop_proxy.plist"; + const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName); + child_process.execSync( + `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`, + ); + } +} + +// Partially based on electron-builder code: +// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/macPackager.ts +// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/codeSign/macCodeSign.ts + +const appleCertificatePrefixes = [ + "Developer ID Application:", + // "Developer ID Installer:", + // "3rd Party Mac Developer Application:", + // "3rd Party Mac Developer Installer:", + "Apple Development:", +]; + +function getIdentities() { + const ids = child_process + .execSync("/usr/bin/security find-identity -v -p codesigning") + .toString(); + + return ids + .split("\n") + .filter((line) => { + for (const prefix of appleCertificatePrefixes) { + if (line.includes(prefix)) { + return true; + } + } + return false; + }) + .map((line) => { + const split = line.trim().split(" "); + const id = split[1]; + const name = split.slice(2).join(" ").replace(/"/g, ""); + return { id, name }; + }); +} + +/** + * @param {import("electron-builder").AfterPackContext} context + */ +async function addElectronFuses(context) { + const platform = context.packager.platform.nodeName; + + const ext = { + darwin: ".app", + win32: ".exe", + linux: "", + }[platform]; + + const IS_LINUX = platform === "linux"; + const executableName = IS_LINUX + ? context.packager.appInfo.productFilename.toLowerCase().replace("-dev", "").replace(" ", "-") + : context.packager.appInfo.productFilename; // .toLowerCase() to accomodate Linux file named `name` but productFileName is `Name` -- Replaces '-dev' because on Linux the executable name is `name` even for the DEV builds + + const electronBinaryPath = path.join(context.appOutDir, `${executableName}${ext}`); + + console.log("## Adding fuses to the electron binary", electronBinaryPath); + + await flipFuses(electronBinaryPath, { + version: FuseVersion.V1, + strictlyRequireAllFuses: true, + resetAdHocDarwinSignature: platform === "darwin" && context.arch === builder.Arch.universal, + + // List of fuses and their default values is available at: + // https://www.electronjs.org/docs/latest/tutorial/fuses + + [FuseV1Options.RunAsNode]: false, + [FuseV1Options.EnableCookieEncryption]: true, + [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, + [FuseV1Options.EnableNodeCliInspectArguments]: false, + + // Currently, asar integrity is only implemented for macOS and Windows + // https://www.electronjs.org/docs/latest/tutorial/asar-integrity + // On macOS, it works by default, but on Windows it requires the + // asarIntegrity feature of electron-builder v25, currently in alpha + // https://github.com/electron-userland/electron-builder/releases/tag/v25.0.0-alpha.10 + [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: platform === "darwin", + + [FuseV1Options.OnlyLoadAppFromAsar]: true, + + // App refuses to open when enabled + [FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false, + + // To disable this, we should stop using the file:// protocol to load the app bundle + // This can be done by defining a custom app:// protocol and loading the bundle from there, + // but then any requests to the server will be blocked by CORS policy + [FuseV1Options.GrantFileProtocolExtraPrivileges]: true, + }); } diff --git a/apps/desktop/src/entry.ts b/apps/desktop/src/entry.ts index 78fe51e8b9e..3bb84461363 100644 --- a/apps/desktop/src/entry.ts +++ b/apps/desktop/src/entry.ts @@ -1,31 +1,33 @@ -import { NativeMessagingProxy } from "./proxy/native-messaging-proxy"; +import { spawn } from "child_process"; +import * as path from "path"; -// We need to import the other dependencies using `require` since `import` will -// generate `Error: Cannot find module 'electron'`. The cause of this error is -// due to native messaging setting the ELECTRON_RUN_AS_NODE env flag on windows -// which removes the electron module. This flag is needed for stdin/out to work -// properly on Windows. +import { app } from "electron"; if ( + process.platform === "darwin" && process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1) ) { - if (process.platform === "darwin") { - // eslint-disable-next-line - const app = require("electron").app; + // If we're on MacOS, we need to support DuckDuckGo's IPC communication, + // which for the moment is launching the Bitwarden process. + // Ideally the browser would instead startup the desktop_proxy process + // when available, but for now we'll just launch it here. - app.on("ready", () => { - app.dock.hide(); - }); - } - - process.stdout.on("error", (e) => { - if (e.code === "EPIPE") { - process.exit(0); - } + app.on("ready", () => { + app.dock.hide(); }); - const proxy = new NativeMessagingProxy(); - proxy.run(); + const proc = spawn(path.join(process.execPath, "..", "desktop_proxy"), process.argv.slice(1), { + cwd: process.cwd(), + stdio: "inherit", + shell: false, + }); + + proc.on("exit", () => { + process.exit(0); + }); + proc.on("error", () => { + process.exit(1); + }); } else { // eslint-disable-next-line const Main = require("./main").Main; diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index cf680c3bd96..723b410f19b 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -227,6 +227,7 @@ export class Main { this.windowMain, app.getPath("userData"), app.getPath("exe"), + app.getAppPath(), ); this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider); @@ -273,13 +274,21 @@ export class Main { if (browserIntegrationEnabled || ddgIntegrationEnabled) { // Re-register the native messaging host integrations on startup, in case they are not present if (browserIntegrationEnabled) { - this.nativeMessagingMain.generateManifests().catch(this.logService.error); + this.nativeMessagingMain + .generateManifests() + .catch((err) => this.logService.error("Error while generating manifests", err)); } if (ddgIntegrationEnabled) { - this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error); + this.nativeMessagingMain + .generateDdgManifests() + .catch((err) => this.logService.error("Error while generating DDG manifests", err)); } - this.nativeMessagingMain.listen(); + this.nativeMessagingMain + .listen() + .catch((err) => + this.logService.error("Error while starting native message listener", err), + ); } app.removeAsDefaultProtocolClient("bitwarden"); diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 8c8404578b6..036f35e61c8 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -1,34 +1,34 @@ import { existsSync, promises as fs } from "fs"; -import { Socket } from "net"; import { homedir, userInfo } from "os"; import * as path from "path"; import * as util from "util"; import { ipcMain } from "electron"; -import * as ipc from "node-ipc"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ipc } from "@bitwarden/desktop-napi"; -import { getIpcSocketRoot } from "../proxy/ipc"; +import { isDev } from "../utils"; import { WindowMain } from "./window.main"; export class NativeMessagingMain { - private connected: Socket[] = []; - private socket: any; + private ipcServer: ipc.IpcServer | null; + private connected: number[] = []; constructor( private logService: LogService, private windowMain: WindowMain, private userPath: string, private exePath: string, + private appPath: string, ) { ipcMain.handle( "nativeMessaging.manifests", async (_event: any, options: { create: boolean }) => { if (options.create) { - this.listen(); try { + await this.listen(); await this.generateManifests(); } catch (e) { this.logService.error("Error generating manifests: " + e); @@ -51,8 +51,8 @@ export class NativeMessagingMain { "nativeMessaging.ddgManifests", async (_event: any, options: { create: boolean }) => { if (options.create) { - this.listen(); try { + await this.listen(); await this.generateDdgManifests(); } catch (e) { this.logService.error("Error generating duckduckgo manifests: " + e); @@ -72,56 +72,46 @@ export class NativeMessagingMain { ); } - listen() { - ipc.config.id = "bitwarden"; - ipc.config.retry = 1500; - const ipcSocketRoot = getIpcSocketRoot(); - if (ipcSocketRoot != null) { - ipc.config.socketRoot = ipcSocketRoot; + async listen() { + if (this.ipcServer) { + this.ipcServer.stop(); } - ipc.serve(() => { - ipc.server.on("message", (data: any, socket: any) => { - this.socket = socket; - this.windowMain.win.webContents.send("nativeMessaging", data); - }); - - ipcMain.on("nativeMessagingReply", (event, msg) => { - if (this.socket != null && msg != null) { - this.send(msg, this.socket); + this.ipcServer = await ipc.IpcServer.listen("bitwarden", (error, msg) => { + switch (msg.kind) { + case ipc.IpcMessageType.Connected: { + this.connected.push(msg.clientId); + this.logService.info("Native messaging client " + msg.clientId + " has connected"); + break; } - }); + case ipc.IpcMessageType.Disconnected: { + const index = this.connected.indexOf(msg.clientId); + if (index > -1) { + this.connected.splice(index, 1); + } - ipc.server.on("connect", (socket: Socket) => { - this.connected.push(socket); - }); - - ipc.server.on("socket.disconnected", (socket, destroyedSocketID) => { - const index = this.connected.indexOf(socket); - if (index > -1) { - this.connected.splice(index, 1); + this.logService.info("Native messaging client " + msg.clientId + " has disconnected"); + break; } - - this.socket = null; - ipc.log("client " + destroyedSocketID + " has disconnected!"); - }); + case ipc.IpcMessageType.Message: + this.windowMain.win.webContents.send("nativeMessaging", JSON.parse(msg.message)); + break; + } }); - ipc.server.start(); - } - - stop() { - ipc.server.stop(); - // Kill all existing connections - this.connected.forEach((socket) => { - if (!socket.destroyed) { - socket.destroy(); + ipcMain.on("nativeMessagingReply", (event, msg) => { + if (msg != null) { + this.send(msg); } }); } - send(message: object, socket: any) { - ipc.server.emit(socket, "message", message); + stop() { + this.ipcServer?.stop(); + } + + send(message: object) { + this.ipcServer?.send(JSON.stringify(message)); } async generateManifests() { @@ -331,11 +321,20 @@ export class NativeMessagingMain { } private binaryPath() { - if (process.platform === "win32") { - return path.join(path.dirname(this.exePath), "resources", "native-messaging.bat"); + const ext = process.platform === "win32" ? ".exe" : ""; + + if (isDev()) { + return path.join( + this.appPath, + "..", + "desktop_native", + "target", + "debug", + `desktop_proxy${ext}`, + ); } - return this.exePath; + return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`); } private getRegeditInstance() { diff --git a/apps/desktop/src/proxy/ipc.ts b/apps/desktop/src/proxy/ipc.ts deleted file mode 100644 index 0160d6bf294..00000000000 --- a/apps/desktop/src/proxy/ipc.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable no-console */ -import { createHash } from "crypto"; -import { existsSync, mkdirSync } from "fs"; -import { homedir } from "os"; -import { join as path_join } from "path"; - -import * as ipc from "node-ipc"; - -export function getIpcSocketRoot(): string | null { - let socketRoot = null; - - switch (process.platform) { - case "darwin": { - const ipcSocketRootDir = path_join(homedir(), "tmp"); - if (!existsSync(ipcSocketRootDir)) { - mkdirSync(ipcSocketRootDir); - } - socketRoot = ipcSocketRootDir + "/"; - break; - } - case "win32": { - // Let node-ipc use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user. - // Hashing prevents problems with reserved characters and file length limitations. - socketRoot = createHash("sha1").update(homedir()).digest("hex") + "."; - } - } - return socketRoot; -} - -ipc.config.id = "proxy"; -ipc.config.retry = 1500; -ipc.config.logger = console.warn; // Stdout is used for native messaging -const ipcSocketRoot = getIpcSocketRoot(); -if (ipcSocketRoot != null) { - ipc.config.socketRoot = ipcSocketRoot; -} - -export default class IPC { - onMessage: (message: object) => void; - - private connected = false; - - connect() { - ipc.connectTo("bitwarden", () => { - ipc.of.bitwarden.on("connect", () => { - this.connected = true; - console.error("## connected to bitwarden desktop ##"); - - // Notify browser extension, connection is established to desktop application. - this.onMessage({ command: "connected" }); - }); - - ipc.of.bitwarden.on("disconnect", () => { - this.connected = false; - console.error("disconnected from world"); - - // Notify browser extension, no connection to desktop application. - this.onMessage({ command: "disconnected" }); - }); - - ipc.of.bitwarden.on("message", (message: any) => { - this.onMessage(message); - }); - - ipc.of.bitwarden.on("error", (err: any) => { - console.error("error", err); - }); - }); - } - - isConnected(): boolean { - return this.connected; - } - - send(json: object) { - ipc.of.bitwarden.emit("message", json); - } -} diff --git a/apps/desktop/src/proxy/native-messaging-proxy.ts b/apps/desktop/src/proxy/native-messaging-proxy.ts deleted file mode 100644 index f1b54a82014..00000000000 --- a/apps/desktop/src/proxy/native-messaging-proxy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import IPC from "./ipc"; -import NativeMessage from "./nativemessage"; - -// Proxy is a lightweight application which provides bi-directional communication -// between the browser extension and a running desktop application. -// -// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop -export class NativeMessagingProxy { - private ipc: IPC; - private nativeMessage: NativeMessage; - - constructor() { - this.ipc = new IPC(); - this.nativeMessage = new NativeMessage(this.ipc); - } - - run() { - this.ipc.connect(); - this.nativeMessage.listen(); - - this.ipc.onMessage = this.nativeMessage.send; - } -} diff --git a/apps/desktop/src/proxy/nativemessage.ts b/apps/desktop/src/proxy/nativemessage.ts deleted file mode 100644 index f7a32296f84..00000000000 --- a/apps/desktop/src/proxy/nativemessage.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable no-console */ -import IPC from "./ipc"; - -// Mostly based on the example from MDN, -// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging -export default class NativeMessage { - ipc: IPC; - - constructor(ipc: IPC) { - this.ipc = ipc; - } - - send(message: object) { - const messageBuffer = Buffer.from(JSON.stringify(message)); - - const headerBuffer = Buffer.alloc(4); - headerBuffer.writeUInt32LE(messageBuffer.length, 0); - - process.stdout.write(Buffer.concat([headerBuffer, messageBuffer])); - } - - listen() { - let payloadSize: number = null; - - // A queue to store the chunks as we read them from stdin. - // This queue can be flushed when `payloadSize` data has been read - const chunks: any = []; - - // Only read the size once for each payload - const sizeHasBeenRead = () => Boolean(payloadSize); - - // All the data has been read, reset everything for the next message - const flushChunksQueue = () => { - payloadSize = null; - chunks.splice(0); - }; - - const processData = () => { - // Create one big buffer with all all the chunks - const stringData = Buffer.concat(chunks); - console.error(stringData); - - // The browser will emit the size as a header of the payload, - // if it hasn't been read yet, do it. - // The next time we'll need to read the payload size is when all of the data - // of the current payload has been read (ie. data.length >= payloadSize + 4) - if (!sizeHasBeenRead()) { - try { - payloadSize = stringData.readUInt32LE(0); - } catch (e) { - console.error(e); - return; - } - } - - // If the data we have read so far is >= to the size advertised in the header, - // it means we have all of the data sent. - // We add 4 here because that's the size of the bytes that old the payloadSize - if (stringData.length >= payloadSize + 4) { - // Remove the header - const contentWithoutSize = stringData.slice(4, payloadSize + 4).toString(); - - // Reset the read size and the queued chunks - flushChunksQueue(); - - const json = JSON.parse(contentWithoutSize); - - // Forward to desktop application - this.ipc.send(json); - } - }; - - process.stdin.on("readable", () => { - // A temporary variable holding the nodejs.Buffer of each - // chunk of data read off stdin - let chunk = null; - - // Read all of the available data - // tslint:disable-next-line:no-conditional-assignment - while ((chunk = process.stdin.read()) !== null) { - chunks.push(chunk); - } - - try { - processData(); - } catch (e) { - console.error(e); - } - }); - - process.stdin.on("end", () => { - process.exit(0); - }); - } -} diff --git a/package-lock.json b/package-lock.json index d760d36e573..c2571510a9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", + "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "@microsoft/signalr": "8.0.7", @@ -5126,6 +5127,33 @@ "node": "*" } }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", diff --git a/package.json b/package.json index b1e69fca494..d2a42da8592 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", + "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "@microsoft/signalr": "8.0.7", From 6e7c83305e85ca1f0fc1ee78fff39174628a9653 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:35:39 -0700 Subject: [PATCH 12/25] [PM-12990]- center align footer buttons (#11342) * center align footer buttons * fix popup-tab-navigation layout --- .../platform/popup/layout/popup-tab-navigation.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html index a0ff252c6c2..972a60d31ad 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -6,7 +6,7 @@ - + - {{ cipher.subTitle }} - + From 256c6aef5ca48be1ee0dbf27f8333b0b3c5138ba Mon Sep 17 00:00:00 2001 From: Alex Yao <33379584+alexyao2015@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:40:28 -0500 Subject: [PATCH 16/25] native-messaging: Add chromium support (#11230) --- apps/desktop/src/main/native-messaging.main.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 036f35e61c8..ec57ecdf7bb 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -201,6 +201,13 @@ export class NativeMessagingMain { chromeJson, ); } + + if (existsSync(`${this.homedir()}/.config/chromium/`)) { + await this.writeManifest( + `${this.homedir()}/.config/chromium/NativeMessagingHosts/com.8bit.bitwarden.json`, + chromeJson, + ); + } break; default: break; From ab5a02f4830ae5f9d1b5c7f6767b40ffabb92820 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:46:10 -0700 Subject: [PATCH 17/25] [PM-12774] - don't display filters when no sends are available (#11298) * don't display filters when no sends are available * move logic down. add conditional class * fix logic for send filters --- apps/browser/src/tools/popup/send-v2/send-v2.component.html | 2 +- libs/tools/send/send-ui/src/services/send-items.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html index 0baa43a4b56..23cc692a598 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -10,7 +10,7 @@ {{ "sendDisabledWarning" | i18n }} - + diff --git a/libs/tools/send/send-ui/src/services/send-items.service.ts b/libs/tools/send/send-ui/src/services/send-items.service.ts index 66ad5b67864..6cef663f891 100644 --- a/libs/tools/send/send-ui/src/services/send-items.service.ts +++ b/libs/tools/send/send-ui/src/services/send-items.service.ts @@ -83,7 +83,7 @@ export class SendItemsService { ); /** - * Observable that indicates whether the user's vault is empty. + * Observable that indicates whether the user's send list is empty. */ emptyList$: Observable = this._sendList$.pipe(map((sends) => !sends.length)); From dab60dbaea503c91840b847871a0cc2d734e6899 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:58:00 -0700 Subject: [PATCH 18/25] [PM-11926] - send created redirect (#11140) * send created redirect * fix test * fix test * fix send form save * return SendData from saveSend * When saving a Send, bubble up a SendView which can be passed to the SendCreated component * Use events to initiate navigation and move actual navigation into client-specific component --------- Co-authored-by: Daniel James Smith --- .../add-edit/send-add-edit.component.html | 3 +- .../add-edit/send-add-edit.component.ts | 18 +++++++++-- .../send-created/send-created.component.html | 7 ++++- .../send-created.component.spec.ts | 11 ++++--- .../send-created/send-created.component.ts | 12 ++++--- .../services/send-api.service.abstraction.ts | 2 +- .../tools/send/services/send-api.service.ts | 3 +- .../components/send-form.component.ts | 31 ++++++++++++------- .../services/default-send-form.service.ts | 3 +- 9 files changed, 62 insertions(+), 28 deletions(-) diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html index b3783bfed3a..40c942539f6 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -4,7 +4,8 @@ diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index c84b9717df1..20b472f97f3 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -2,12 +2,13 @@ import { CommonModule, Location } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; -import { ActivatedRoute, Params } from "@angular/router"; +import { ActivatedRoute, Params, Router } from "@angular/router"; import { map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendId } from "@bitwarden/common/types/guid"; import { @@ -95,14 +96,25 @@ export class SendAddEditComponent { private sendApiService: SendApiService, private toastService: ToastService, private dialogService: DialogService, + private router: Router, ) { this.subscribeToParams(); } /** - * Handles the event when the send is saved. + * Handles the event when the send is created. */ - onSendSaved() { + async onSendCreated(send: SendView) { + await this.router.navigate(["/send-created"], { + queryParams: { sendId: send.id }, + }); + return; + } + + /** + * Handles the event when the send is updated. + */ + onSendUpdated(send: SendView) { this.location.back(); } diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index 9b56fa74d91..c97d3da1396 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -1,6 +1,11 @@
- + diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index 413f22565e1..bcc4d2e2ccb 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -1,6 +1,6 @@ import { CommonModule, Location } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; @@ -33,6 +33,7 @@ describe("SendCreatedComponent", () => { let location: MockProxy; let activatedRoute: MockProxy; let environmentService: MockProxy; + let router: MockProxy; const sendId = "test-send-id"; const deletionDate = new Date(); @@ -52,6 +53,7 @@ describe("SendCreatedComponent", () => { location = mock(); activatedRoute = mock(); environmentService = mock(); + router = mock(); Object.defineProperty(environmentService, "environment$", { configurable: true, get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })), @@ -89,6 +91,7 @@ describe("SendCreatedComponent", () => { { provide: ConfigService, useValue: mock() }, { provide: EnvironmentService, useValue: environmentService }, { provide: PopupRouterCacheService, useValue: mock() }, + { provide: Router, useValue: router }, ], }).compileComponents(); }); @@ -109,10 +112,10 @@ describe("SendCreatedComponent", () => { expect(component["daysAvailable"]).toBe(7); }); - it("should navigate back on close", () => { + it("should navigate back to send list on close", async () => { fixture.detectChanges(); - component.close(); - expect(location.back).toHaveBeenCalled(); + await component.close(); + expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]); }); describe("getDaysAvailable", () => { diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index 92339774d05..4ed4da2f81d 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -1,7 +1,7 @@ -import { CommonModule, Location } from "@angular/common"; +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -30,6 +30,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page PopupHeaderComponent, PopupPageComponent, RouterLink, + RouterModule, PopupFooterComponent, IconModule, ], @@ -45,10 +46,11 @@ export class SendCreatedComponent { private sendService: SendService, private route: ActivatedRoute, private toastService: ToastService, - private location: Location, + private router: Router, private environmentService: EnvironmentService, ) { const sendId = this.route.snapshot.queryParamMap.get("sendId"); + this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => { this.send = sendViews.find((s) => s.id === sendId); if (this.send) { @@ -62,8 +64,8 @@ export class SendCreatedComponent { return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24))); } - close() { - this.location.back(); + async close() { + await this.router.navigate(["/tabs/send"]); } async copyLink() { diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index 100985c4870..4109df19680 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -36,5 +36,5 @@ export abstract class SendApiService { renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise; removePassword: (id: string) => Promise; delete: (id: string) => Promise; - save: (sendData: [Send, EncArrayBuffer]) => Promise; + save: (sendData: [Send, EncArrayBuffer]) => Promise; } diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index 2cb2ff1c2f0..ff71408bce3 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -135,11 +135,12 @@ export class SendApiService implements SendApiServiceAbstraction { return this.apiService.send("DELETE", "/sends/" + id, null, true, false); } - async save(sendData: [Send, EncArrayBuffer]): Promise { + async save(sendData: [Send, EncArrayBuffer]): Promise { const response = await this.upload(sendData); const data = new SendData(response); await this.sendService.upsert(data); + return new Send(data); } async delete(id: string): Promise { diff --git a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts index 1d93804e11f..07939ccb06c 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts @@ -85,9 +85,14 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send submitBtn?: ButtonComponent; /** - * Event emitted when the send is saved successfully. + * Event emitted when the send is created successfully. */ - @Output() sendSaved = new EventEmitter(); + @Output() onSendCreated = new EventEmitter(); + + /** + * Event emitted when the send is updated successfully. + */ + @Output() onSendUpdated = new EventEmitter(); /** * The original send being edited or cloned. Null for add mode. @@ -200,22 +205,26 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send return; } + const sendView = await this.addEditFormService.saveSend( + this.updatedSendView, + this.file, + this.config, + ); + + if (this.config.mode === "add") { + this.onSendCreated.emit(sendView); + return; + } + if (Utils.isNullOrWhitespace(this.updatedSendView.password)) { this.updatedSendView.password = null; } - await this.addEditFormService.saveSend(this.updatedSendView, this.file, this.config); - this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t( - this.config.mode === "edit" || this.config.mode === "partial-edit" - ? "editedItem" - : "addedItem", - ), + message: this.i18nService.t("editedItem"), }); - - this.sendSaved.emit(this.updatedSendView); + this.onSendUpdated.emit(this.updatedSendView); }; } diff --git a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts index 9b6a6360ac7..9eb37b07e50 100644 --- a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts +++ b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts @@ -19,6 +19,7 @@ export class DefaultSendFormService implements SendFormService { async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) { const sendData = await this.sendService.encrypt(send, file, send.password, null); - return await this.sendApiService.save(sendData); + const newSend = await this.sendApiService.save(sendData); + return await this.decryptSend(newSend); } } From 9ff1db757318a81c594d40849c4bbe01ab5d8193 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:06:18 -0400 Subject: [PATCH 19/25] Auth/PM-9449 - UI Refresh + Client component consolidation into new LockV2 Component (#10451) * PM-9449 - Init stub of new lock comp * PM-9449 - (1) Add new lock screen title to all clients (2) Add to temp web routing module config * PM-9449 - LockV2Comp - Building now with web HTML * PM-9449 - Libs/Auth LockComp - bring in all desktop ts code; WIP, need to stand up LockCompService to facilitate ipc communication. * PM-9449 - Create LockComponentService for facilitating client logic; potentially will decompose later. * PM-9449 - Add extension lock comp service. * PM-9449 - Libs/auth LockComp - bring in browser extension logic * PM-9449 - Libs/auth LockComp html start * PM-9449 - Libs/Auth LockComp - (1) Remove unused dep (2) Update setEmailAsPageSubtitle to work. * PM-9449 - Add getBiometricsError to lock comp service for extension. * PM-9449 - LockComp - (1) Save off client type as public comp var (2) Rename biometricLock as biometricLockSet * PM-9449 - Work on lock comp service getAvailableUnlockOptions * PM-9449 - WIP libs/auth LockComp * PM-9449 - (1) Remove default lock comp svc (2) Add web lock comp svc. * PM-9449 - UnlockOptions - replace incorrect type * PM-9449 - DesktopLockComponentService -get most of observable based getAvailableUnlockOptions$ logic in place. * PM-9449 - LockCompSvc - getAvailableUnlockOptions in place for all clients. * PM-9449 - Add getBiometricsUnlockBtnText to LockCompSvc and put TODO for wiring it up later * PM-9449 - Lock Comp - Replace all manual bools with unlock options. * PM-9449 - Desktop Lock Comp Svc - adjust spacing * PM-9449 - LockCompSvc - remove biometricsEnabled method * PM-9449 - LockComp - Clean up commented out code * PM-9449 - LockComp - webVaultHostname --> envHostName * PM-9449 - Fix lock comp svc deps * PM-9449 - LockComp - HTML progress * PM-9449 - LockComp cleanup * PM-9449 - Web Routing Module - wire up lock vs lockv2 using extension swap * PM-9449 - Wire up loading state * PM-9449 - LockComp - start wiring up listenForActiveUnlockOptionChanges logic with reactivity * PM-9449 - Update desktop & extension lock comp service to use new biometrics service vs platform utils for biometrics information. * PM-9449 - LockV2 - Swap platform util usage with toast svc * PM-9449 - LockV2Comp - Bring over user id logic from PM-8933 * PM-9449 - LockV2Comp - Adjust everything to use activeAccount.id. * PM-9449 - LockV2Comp - Progress on wiring up unlock option reactive stream. * PM-9449 - LockComp ts - some refactoring and minor progress. * PM-9449 - LockComp HTML - refactoring based on new idea to keep unlock options as separate as possible. * PM-9449 - Add PIN translation to web * PM-9449 - (1) Lock HTML refactor to make as independent verticals as possible (2) Refactor Lock ts (3) LockSvc - replace type with enum. * PM-9449 - LockV2Comp - remove hardcoded await. * PM-9449 - LockComp HTML - add todo * PM-9449 - Web - Routing module - cleanup commented out stuff * PM-9449 - LockV2Comp - Wire up biometrics + mild refactor. * PM-9449 - Desktop - Wire up lockV2 redirection * PM-9449 - LockV2 - Desktop - don't focus until unlock opts defined. * PM-9449 - Fix accidental check in * PM-9449 - LockV2 - loading state depends on unlock opts * PM-9449 - LockV2 comp - remove unnecessary hr * PM-9449 - Migrate "yourVaultIsLockedV2" translation to desktop & browser. * PM-9449 - LockV2 - Layout tweaks for biometrics * PM-9449 - LockV2 - Biometric btn text * PM-9449 - LockV2 - Wire up biometrics loading / disable state + remove unnecessary conditions around biometricsUnlockBtnText * PM-9449 - DesktopLockSvc - Per discussion with Bernd, remove interval polling and just check once for biometric support and availability. * PM-9449 - AuthGuard - Add todo to remove promptBiometric * PM-9449 - LockV2 - Refactor primary and desktop init logic + misc clean up * PM-9449 - LockV2 - Reorder init methods * PM-9449 - LockV2 - Per discussion with Product, deprecate windows biometric settings update warning * PM-9449 - Add TODO per discussion with Justin and remove TODO * PM-9449 - LockV2 - Restore hide password on desktop window hidden functionality. * PM-9449 - Clean up accomplished todo * PM-9449 - LockV2 - Refactor func name. * PM-9449 - LockV2 Comp - (1) TODO cleanup (2) Add browser logic to handleBiometricsUnlockEnabled * PM-9449 - LockCompSvc changes - (1) Observability for isFido2Session (2) Adjust errors and returns per discussion with Justin * PM-9449 - Per product, no longer need to support special fido2 case on extension. * PM-9449 - LockCompSvc - add getPreviousUrl support * PM-9449 - LockV2 - Continued ts cleanup * PM-9449 - LockV2Comp - clean up unused props * PM-9449 - LockV2Comp - Rename response to masterPasswordVerificationResponse * PM-9449 - LockV2 - Remove unused formPromise prop * PM-9449 - Add missing translations + update desktop to showReadonlyHostName * PM-9449 - LockV2 - cleanup TODO * PM-9449 - LockV2 - more cleanup * PM-9449 - Desktop Routing Module - only allow LockV2 access if extension refresh flag is enabled. * PM-9449 - Extension - AppRoutingModule - Add extension redirect + new lockV2 route. * PM-9449 - Extension - AppRoutingModule - Add lockV2 to the ExtensionAnonLayoutWrapperComponent intead of the regular one. * PM-9449 - Extension - CurrentAccountComp - add null checks as anon layout components don't have a state today. This prevents the account switcher from working on the new lockV2 comp. * PM-9449 - Extension AppRoutingModule - LockV2 should use ExtensionAnonLayoutWrapperData * PM-9449 - LockComp - BiometricUnlock - cancelling is a valid action. * PM-9449 - LockV2 - Biometric autoprompt cleanup * PM-9449 - LockV2 - (1) Add TODO for KM team (2) Fix submit logic. * PM-9449 - Tweak TODO to add task # * PM-9449 - Test WebLockComponentService * PM-9449 - ExtensionLockComponentService tested * PM-9449 - Tweak extension lock comp svc test * PM-9449 - DesktopLockComponentService tested * PM-9449 - Add task # to TODO * PM-9449 - Update apps/browser/src/services/extension-lock-component.service.ts per PR feedback Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * PM-9449 - Per PR feedback, replace from with defer for better reactive execution of promise based functions. * PM-9449 - Per PR feedback replace enum with type. * PM-9449 - Fix imports and tests due to key management file moves. * PM-9449 - Another test file import fix --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- apps/browser/src/_locales/en/messages.json | 15 + .../current-account.component.ts | 2 +- apps/browser/src/popup/app-routing.module.ts | 25 + .../src/popup/services/services.module.ts | 8 +- .../extension-lock-component.service.spec.ts | 325 +++++++++ .../extension-lock-component.service.ts | 117 ++++ apps/desktop/src/app/app-routing.module.ts | 19 + .../src/app/services/services.module.ts | 8 +- apps/desktop/src/locales/en/messages.json | 18 + .../desktop-lock-component.service.spec.ts | 377 +++++++++++ .../desktop-lock-component.service.ts | 129 ++++ apps/web/src/app/auth/core/services/index.ts | 1 + .../web-lock-component.service.spec.ts | 94 +++ .../services/web-lock-component.service.ts | 55 ++ apps/web/src/app/core/core.module.ts | 12 +- apps/web/src/app/oss-routing.module.ts | 52 +- apps/web/src/locales/en/messages.json | 20 +- libs/angular/src/auth/guards/auth.guard.ts | 2 + libs/auth/src/angular/index.ts | 4 + .../angular/lock/lock-component.service.ts | 48 ++ .../auth/src/angular/lock/lock.component.html | 191 ++++++ libs/auth/src/angular/lock/lock.component.ts | 638 ++++++++++++++++++ 22 files changed, 2139 insertions(+), 21 deletions(-) create mode 100644 apps/browser/src/services/extension-lock-component.service.spec.ts create mode 100644 apps/browser/src/services/extension-lock-component.service.ts create mode 100644 apps/desktop/src/services/desktop-lock-component.service.spec.ts create mode 100644 apps/desktop/src/services/desktop-lock-component.service.ts create mode 100644 apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts create mode 100644 apps/web/src/app/auth/core/services/web-lock-component.service.ts create mode 100644 libs/auth/src/angular/lock/lock-component.service.ts create mode 100644 libs/auth/src/angular/lock/lock.component.html create mode 100644 libs/auth/src/angular/lock/lock.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5203edf0a44..ec0fac137df 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -604,6 +604,15 @@ "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, + "yourVaultIsLockedV2": { + "message": "Your vault is locked" + }, + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "or": { + "message": "or" + }, "unlock": { "message": "Unlock" }, @@ -1936,6 +1945,9 @@ "unlockWithBiometrics": { "message": "Unlock with biometrics" }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "awaitDesktop": { "message": "Awaiting confirmation from desktop" }, @@ -3623,6 +3635,9 @@ "typePasskey": { "message": "Passkey" }, + "accessing": { + "message": "Accessing" + }, "passkeyNotCopied": { "message": "Passkey will not be copied" }, diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 6c7c1e7d92f..12210b2b452 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -59,7 +59,7 @@ export class CurrentAccountComponent { } async currentAccountClicked() { - if (this.route.snapshot.data.state.includes("account-switcher")) { + if (this.route.snapshot.data?.state?.includes("account-switcher")) { this.location.back(); } else { await this.router.navigate(["/account-switcher"]); diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index d540ea39edc..9fd52470c0a 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -17,6 +17,8 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + LockIcon, + LockV2Component, PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, @@ -181,6 +183,7 @@ const routes: Routes = [ path: "lock", component: LockComponent, canActivate: [lockGuard()], + canMatch: [extensionRefreshRedirect("/lockV2")], data: { state: "lock", doNotSaveUrl: true } satisfies RouteDataProperties, }, ...twofactorRefactorSwap( @@ -438,6 +441,28 @@ const routes: Routes = [ ], }, ), + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "lockV2", + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + data: { + pageIcon: LockIcon, + pageTitle: "yourVaultIsLockedV2", + showReadonlyHostname: true, + showAcctSwitcher: true, + } satisfies ExtensionAnonLayoutWrapperData, + children: [ + { + path: "", + component: LockV2Component, + }, + ], + }, + ], + }, { path: "", component: AnonLayoutWrapperComponent, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 483bf86712a..024b4f46315 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -16,7 +16,7 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; +import { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular"; import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; @@ -117,6 +117,7 @@ import { ForegroundTaskSchedulerService } from "../../platform/services/task-sch import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; +import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; @@ -536,6 +537,11 @@ const safeProviders: SafeProvider[] = [ provide: CLIENT_TYPE, useValue: ClientType.Browser, }), + safeProvider({ + provide: LockComponentService, + useClass: ExtensionLockComponentService, + deps: [], + }), safeProvider({ provide: Fido2UserVerificationService, useClass: Fido2UserVerificationService, diff --git a/apps/browser/src/services/extension-lock-component.service.spec.ts b/apps/browser/src/services/extension-lock-component.service.spec.ts new file mode 100644 index 00000000000..f537897cf8d --- /dev/null +++ b/apps/browser/src/services/extension-lock-component.service.spec.ts @@ -0,0 +1,325 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; + +import { ExtensionLockComponentService } from "./extension-lock-component.service"; + +describe("ExtensionLockComponentService", () => { + let service: ExtensionLockComponentService; + + let userDecryptionOptionsService: MockProxy; + let platformUtilsService: MockProxy; + let biometricsService: MockProxy; + let pinService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; + let cryptoService: MockProxy; + let routerService: MockProxy; + + beforeEach(() => { + userDecryptionOptionsService = mock(); + platformUtilsService = mock(); + biometricsService = mock(); + pinService = mock(); + vaultTimeoutSettingsService = mock(); + cryptoService = mock(); + routerService = mock(); + + TestBed.configureTestingModule({ + providers: [ + ExtensionLockComponentService, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: userDecryptionOptionsService, + }, + { + provide: PlatformUtilsService, + useValue: platformUtilsService, + }, + { + provide: BiometricsService, + useValue: biometricsService, + }, + { + provide: PinServiceAbstraction, + useValue: pinService, + }, + { + provide: VaultTimeoutSettingsService, + useValue: vaultTimeoutSettingsService, + }, + { + provide: CryptoService, + useValue: cryptoService, + }, + { + provide: BrowserRouterService, + useValue: routerService, + }, + ], + }); + + service = TestBed.inject(ExtensionLockComponentService); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + describe("getPreviousUrl", () => { + it("returns the previous URL", () => { + routerService.getPreviousUrl.mockReturnValue("previousUrl"); + expect(service.getPreviousUrl()).toBe("previousUrl"); + }); + }); + + describe("getBiometricsError", () => { + it("returns a biometric error description when given a valid error type", () => { + expect( + service.getBiometricsError({ + message: "startDesktop", + }), + ).toBe("startDesktopDesc"); + }); + + it("returns null when given an invalid error type", () => { + expect( + service.getBiometricsError({ + message: "invalidError", + }), + ).toBeNull(); + }); + + it("returns null when given a null input", () => { + expect(service.getBiometricsError(null)).toBeNull(); + }); + }); + + describe("isWindowVisible", () => { + it("throws an error", async () => { + await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); + }); + }); + + describe("getBiometricsUnlockBtnText", () => { + it("returns the biometric unlock button text", () => { + expect(service.getBiometricsUnlockBtnText()).toBe("unlockWithBiometrics"); + }); + }); + + describe("getAvailableUnlockOptions$", () => { + interface MockInputs { + hasMasterPassword: boolean; + osSupportsBiometric: boolean; + biometricLockSet: boolean; + hasBiometricEncryptedUserKeyStored: boolean; + platformSupportsSecureStorage: boolean; + pinDecryptionAvailable: boolean; + } + + const table: [MockInputs, UnlockOptions][] = [ + [ + // MP + PIN + Biometrics available + { + hasMasterPassword: true, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: true, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // PIN + Biometrics available + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: no user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics not available: biometric lock not set + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: false, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: user key not stored + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: OS doesn't support + { + hasMasterPassword: false, + osSupportsBiometric: false, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem, + }, + }, + ], + ]; + + test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => { + const userId = "userId" as UserId; + const userDecryptionOptions = { + hasMasterPassword: mockInputs.hasMasterPassword, + }; + + // MP + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); + + // Biometrics + biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet); + cryptoService.hasUserKeyStored.mockResolvedValue( + mockInputs.hasBiometricEncryptedUserKeyStored, + ); + platformUtilsService.supportsSecureStorage.mockReturnValue( + mockInputs.platformSupportsSecureStorage, + ); + + // PIN + pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); + + const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId)); + + expect(unlockOptions).toEqual(expectedOutput); + }); + }); +}); diff --git a/apps/browser/src/services/extension-lock-component.service.ts b/apps/browser/src/services/extension-lock-component.service.ts new file mode 100644 index 00000000000..58514fa2b17 --- /dev/null +++ b/apps/browser/src/services/extension-lock-component.service.ts @@ -0,0 +1,117 @@ +import { inject } from "@angular/core"; +import { combineLatest, defer, map, Observable } from "rxjs"; + +import { + BiometricsDisableReason, + LockComponentService, + UnlockOptions, +} from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors"; +import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; + +export class ExtensionLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + private readonly platformUtilsService = inject(PlatformUtilsService); + private readonly biometricsService = inject(BiometricsService); + private readonly pinService = inject(PinServiceAbstraction); + private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); + private readonly cryptoService = inject(CryptoService); + private readonly routerService = inject(BrowserRouterService); + + getPreviousUrl(): string | null { + return this.routerService.getPreviousUrl(); + } + + getBiometricsError(error: any): string | null { + const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes]; + + if (!biometricsError) { + return null; + } + + return biometricsError.description; + } + + async isWindowVisible(): Promise { + throw new Error("Method not implemented."); + } + + getBiometricsUnlockBtnText(): string { + return "unlockWithBiometrics"; + } + + private async isBiometricLockSet(userId: UserId): Promise { + const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId); + const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored( + KeySuffixOptions.Biometric, + userId, + ); + const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); + + return ( + biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage) + ); + } + + private getBiometricsDisabledReason( + osSupportsBiometric: boolean, + biometricLockSet: boolean, + ): BiometricsDisableReason | null { + if (!osSupportsBiometric) { + return BiometricsDisableReason.NotSupportedOnOperatingSystem; + } else if (!biometricLockSet) { + return BiometricsDisableReason.EncryptedKeysUnavailable; + } + + return null; + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return combineLatest([ + // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to + defer(() => this.biometricsService.supportsBiometric()), + defer(() => this.isBiometricLockSet(userId)), + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + defer(() => this.pinService.isPinDecryptionAvailable(userId)), + ]).pipe( + map( + ([ + supportsBiometric, + isBiometricsLockSet, + userDecryptionOptions, + pinDecryptionAvailable, + ]) => { + const disableReason = this.getBiometricsDisabledReason( + supportsBiometric, + isBiometricsLockSet, + ); + + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: pinDecryptionAvailable, + }, + biometrics: { + enabled: supportsBiometric && isBiometricsLockSet, + disableReason: disableReason, + }, + }; + return unlockOpts; + }, + ), + ); + } +} diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 1e13be12a73..86a39163f3a 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -11,9 +11,12 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + LockIcon, + LockV2Component, PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, @@ -62,6 +65,7 @@ const routes: Routes = [ path: "lock", component: LockComponent, canActivate: [lockGuard()], + canMatch: [extensionRefreshRedirect("/lockV2")], }, { path: "login", @@ -190,6 +194,21 @@ const routes: Routes = [ }, ], }, + { + path: "lockV2", + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + data: { + pageIcon: LockIcon, + pageTitle: "yourVaultIsLockedV2", + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + children: [ + { + path: "", + component: LockV2Component, + }, + ], + }, { path: "set-password-jit", canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index d3d41d277b6..c6b73fbbbca 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -19,7 +19,7 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { SetPasswordJitService } from "@bitwarden/auth/angular"; +import { LockComponentService, SetPasswordJitService } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction, PinServiceAbstraction, @@ -86,6 +86,7 @@ import { ElectronRendererStorageService } from "../../platform/services/electron import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging"; import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme"; +import { DesktopLockComponentService } from "../../services/desktop-lock-component.service"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; import { NativeMessagingService } from "../../services/native-messaging.service"; @@ -277,6 +278,11 @@ const safeProviders: SafeProvider[] = [ useClass: NativeMessagingManifestService, deps: [], }), + safeProvider({ + provide: LockComponentService, + useClass: DesktopLockComponentService, + deps: [], + }), safeProvider({ provide: CLIENT_TYPE, useValue: ClientType.Desktop, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 9504ecb1fa3..0b7a9c678c9 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -918,6 +918,18 @@ "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "or": { + "message": "or" + }, + "unlockWithBiometrics": { + "message": "Unlock with biometrics" + }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "unlock": { "message": "Unlock" }, @@ -2256,6 +2268,9 @@ "locked": { "message": "Locked" }, + "yourVaultIsLockedV2": { + "message": "Your vault is locked" + }, "unlocked": { "message": "Unlocked" }, @@ -2608,6 +2623,9 @@ "important": { "message": "Important:" }, + "accessing": { + "message": "Accessing" + }, "accessTokenUnableToBeDecrypted": { "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." }, diff --git a/apps/desktop/src/services/desktop-lock-component.service.spec.ts b/apps/desktop/src/services/desktop-lock-component.service.spec.ts new file mode 100644 index 00000000000..ff1f8328ea3 --- /dev/null +++ b/apps/desktop/src/services/desktop-lock-component.service.spec.ts @@ -0,0 +1,377 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { DesktopLockComponentService } from "./desktop-lock-component.service"; + +// ipc mock global +const isWindowVisibleMock = jest.fn(); +const biometricEnabledMock = jest.fn(); +(global as any).ipc = { + keyManagement: { + biometric: { + enabled: biometricEnabledMock, + }, + }, + platform: { + isWindowVisible: isWindowVisibleMock, + }, +}; + +describe("DesktopLockComponentService", () => { + let service: DesktopLockComponentService; + + let userDecryptionOptionsService: MockProxy; + let platformUtilsService: MockProxy; + let biometricsService: MockProxy; + let pinService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; + let cryptoService: MockProxy; + + beforeEach(() => { + userDecryptionOptionsService = mock(); + platformUtilsService = mock(); + biometricsService = mock(); + pinService = mock(); + vaultTimeoutSettingsService = mock(); + cryptoService = mock(); + + TestBed.configureTestingModule({ + providers: [ + DesktopLockComponentService, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: userDecryptionOptionsService, + }, + { + provide: PlatformUtilsService, + useValue: platformUtilsService, + }, + { + provide: BiometricsService, + useValue: biometricsService, + }, + { + provide: PinServiceAbstraction, + useValue: pinService, + }, + { + provide: VaultTimeoutSettingsService, + useValue: vaultTimeoutSettingsService, + }, + { + provide: CryptoService, + useValue: cryptoService, + }, + ], + }); + + service = TestBed.inject(DesktopLockComponentService); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + // getBiometricsError + describe("getBiometricsError", () => { + it("returns null when given null", () => { + const result = service.getBiometricsError(null); + expect(result).toBeNull(); + }); + + it("returns null when given an unknown error", () => { + const result = service.getBiometricsError({ message: "unknown" }); + expect(result).toBeNull(); + }); + }); + + describe("getPreviousUrl", () => { + it("returns null", () => { + const result = service.getPreviousUrl(); + expect(result).toBeNull(); + }); + }); + + describe("isWindowVisible", () => { + it("returns the window visibility", async () => { + isWindowVisibleMock.mockReturnValue(true); + const result = await service.isWindowVisible(); + expect(result).toBe(true); + }); + }); + + describe("getBiometricsUnlockBtnText", () => { + it("returns the correct text for Mac OS", () => { + platformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop); + const result = service.getBiometricsUnlockBtnText(); + expect(result).toBe("unlockWithTouchId"); + }); + + it("returns the correct text for Windows", () => { + platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); + const result = service.getBiometricsUnlockBtnText(); + expect(result).toBe("unlockWithWindowsHello"); + }); + + it("returns the correct text for Linux", () => { + platformUtilsService.getDevice.mockReturnValue(DeviceType.LinuxDesktop); + const result = service.getBiometricsUnlockBtnText(); + expect(result).toBe("unlockWithPolkit"); + }); + + it("throws an error for an unsupported platform", () => { + platformUtilsService.getDevice.mockReturnValue("unsupported" as any); + expect(() => service.getBiometricsUnlockBtnText()).toThrowError("Unsupported platform"); + }); + }); + + describe("getAvailableUnlockOptions$", () => { + interface MockInputs { + hasMasterPassword: boolean; + osSupportsBiometric: boolean; + biometricLockSet: boolean; + biometricReady: boolean; + hasBiometricEncryptedUserKeyStored: boolean; + platformSupportsSecureStorage: boolean; + pinDecryptionAvailable: boolean; + } + + const table: [MockInputs, UnlockOptions][] = [ + [ + // MP + PIN + Biometrics available + { + hasMasterPassword: true, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: true, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // PIN + Biometrics available + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: no user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + biometricReady: true, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics not available: biometric not ready + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: false, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.SystemBiometricsUnavailable, + }, + }, + ], + [ + // Biometrics not available: biometric lock not set + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: false, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: user key not stored + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: OS doesn't support + { + hasMasterPassword: false, + osSupportsBiometric: false, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem, + }, + }, + ], + ]; + + test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => { + const userId = "userId" as UserId; + const userDecryptionOptions = { + hasMasterPassword: mockInputs.hasMasterPassword, + }; + + // MP + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); + + // Biometrics + biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet); + cryptoService.hasUserKeyStored.mockResolvedValue( + mockInputs.hasBiometricEncryptedUserKeyStored, + ); + platformUtilsService.supportsSecureStorage.mockReturnValue( + mockInputs.platformSupportsSecureStorage, + ); + biometricEnabledMock.mockResolvedValue(mockInputs.biometricReady); + + // PIN + pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); + + const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId)); + + expect(unlockOptions).toEqual(expectedOutput); + }); + }); +}); diff --git a/apps/desktop/src/services/desktop-lock-component.service.ts b/apps/desktop/src/services/desktop-lock-component.service.ts new file mode 100644 index 00000000000..f31ee93a726 --- /dev/null +++ b/apps/desktop/src/services/desktop-lock-component.service.ts @@ -0,0 +1,129 @@ +import { inject } from "@angular/core"; +import { combineLatest, defer, map, Observable } from "rxjs"; + +import { + BiometricsDisableReason, + LockComponentService, + UnlockOptions, +} from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +export class DesktopLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + private readonly platformUtilsService = inject(PlatformUtilsService); + private readonly biometricsService = inject(BiometricsService); + private readonly pinService = inject(PinServiceAbstraction); + private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); + private readonly cryptoService = inject(CryptoService); + + constructor() {} + + getBiometricsError(error: any): string | null { + return null; + } + + getPreviousUrl(): string | null { + return null; + } + + async isWindowVisible(): Promise { + return ipc.platform.isWindowVisible(); + } + + getBiometricsUnlockBtnText(): string { + switch (this.platformUtilsService.getDevice()) { + case DeviceType.MacOsDesktop: + return "unlockWithTouchId"; + case DeviceType.WindowsDesktop: + return "unlockWithWindowsHello"; + case DeviceType.LinuxDesktop: + return "unlockWithPolkit"; + default: + throw new Error("Unsupported platform"); + } + } + + private async isBiometricLockSet(userId: UserId): Promise { + const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId); + const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored( + KeySuffixOptions.Biometric, + userId, + ); + const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); + + return ( + biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage) + ); + } + + private async isBiometricsSupportedAndReady( + userId: UserId, + ): Promise<{ supportsBiometric: boolean; biometricReady: boolean }> { + const supportsBiometric = await this.biometricsService.supportsBiometric(); + const biometricReady = await ipc.keyManagement.biometric.enabled(userId); + return { supportsBiometric, biometricReady }; + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return combineLatest([ + // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to + defer(() => this.isBiometricsSupportedAndReady(userId)), + defer(() => this.isBiometricLockSet(userId)), + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + defer(() => this.pinService.isPinDecryptionAvailable(userId)), + ]).pipe( + map( + ([biometricsData, isBiometricsLockSet, userDecryptionOptions, pinDecryptionAvailable]) => { + const disableReason = this.getBiometricsDisabledReason( + biometricsData.supportsBiometric, + isBiometricsLockSet, + biometricsData.biometricReady, + ); + + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: pinDecryptionAvailable, + }, + biometrics: { + enabled: + biometricsData.supportsBiometric && + isBiometricsLockSet && + biometricsData.biometricReady, + disableReason: disableReason, + }, + }; + + return unlockOpts; + }, + ), + ); + } + + private getBiometricsDisabledReason( + osSupportsBiometric: boolean, + biometricLockSet: boolean, + biometricReady: boolean, + ): BiometricsDisableReason | null { + if (!osSupportsBiometric) { + return BiometricsDisableReason.NotSupportedOnOperatingSystem; + } else if (!biometricLockSet) { + return BiometricsDisableReason.EncryptedKeysUnavailable; + } else if (!biometricReady) { + return BiometricsDisableReason.SystemBiometricsUnavailable; + } + return null; + } +} diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts index c85f0f3204c..9e433b87f36 100644 --- a/apps/web/src/app/auth/core/services/index.ts +++ b/apps/web/src/app/auth/core/services/index.ts @@ -1,3 +1,4 @@ export * from "./webauthn-login"; export * from "./set-password-jit"; export * from "./registration"; +export * from "./web-lock-component.service"; diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts b/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts new file mode 100644 index 00000000000..5eb26a8c76c --- /dev/null +++ b/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts @@ -0,0 +1,94 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { WebLockComponentService } from "./web-lock-component.service"; + +describe("WebLockComponentService", () => { + let service: WebLockComponentService; + + let userDecryptionOptionsService: MockProxy; + + beforeEach(() => { + userDecryptionOptionsService = mock(); + + TestBed.configureTestingModule({ + providers: [ + WebLockComponentService, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: userDecryptionOptionsService, + }, + ], + }); + + service = TestBed.inject(WebLockComponentService); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + describe("getBiometricsError", () => { + it("throws an error when given a null input", () => { + expect(() => service.getBiometricsError(null)).toThrow( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + }); + it("throws an error when given a non-null input", () => { + expect(() => service.getBiometricsError("error")).toThrow( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + }); + }); + + describe("getPreviousUrl", () => { + it("returns null", () => { + expect(service.getPreviousUrl()).toBeNull(); + }); + }); + + describe("isWindowVisible", () => { + it("throws an error", async () => { + await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); + }); + }); + + describe("getBiometricsUnlockBtnText", () => { + it("throws an error", () => { + expect(() => service.getBiometricsUnlockBtnText()).toThrow( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + }); + }); + + describe("getAvailableUnlockOptions$", () => { + it("returns an observable of unlock options", async () => { + const userId = "user-id" as UserId; + const userDecryptionOptions = { + hasMasterPassword: true, + }; + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValueOnce( + of(userDecryptionOptions), + ); + + const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId)); + + expect(unlockOptions).toEqual({ + masterPassword: { + enabled: true, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: null, + }, + }); + }); + }); +}); diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.ts b/apps/web/src/app/auth/core/services/web-lock-component.service.ts new file mode 100644 index 00000000000..e24f299e23b --- /dev/null +++ b/apps/web/src/app/auth/core/services/web-lock-component.service.ts @@ -0,0 +1,55 @@ +import { inject } from "@angular/core"; +import { map, Observable } from "rxjs"; + +import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular"; +import { + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { UserId } from "@bitwarden/common/types/guid"; + +export class WebLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + + constructor() {} + + getBiometricsError(error: any): string | null { + throw new Error( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + } + + getPreviousUrl(): string | null { + return null; + } + + async isWindowVisible(): Promise { + throw new Error("Method not implemented."); + } + + getBiometricsUnlockBtnText(): string { + throw new Error( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( + map((userDecryptionOptions: UserDecryptionOptions) => { + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: null, + }, + }; + return unlockOpts; + }), + ); + } +} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 419794fe3bc..c14c9750474 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -20,6 +20,7 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { RegistrationFinishService as RegistrationFinishServiceAbstraction, + LockComponentService, SetPasswordJitService, } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -62,7 +63,11 @@ import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/va import { BiometricsService } from "@bitwarden/key-management"; import { PolicyListService } from "../admin-console/core/policy-list.service"; -import { WebRegistrationFinishService, WebSetPasswordJitService } from "../auth"; +import { + WebSetPasswordJitService, + WebRegistrationFinishService, + WebLockComponentService, +} from "../auth"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; @@ -197,6 +202,11 @@ const safeProviders: SafeProvider[] = [ PolicyService, ], }), + safeProvider({ + provide: LockComponentService, + useClass: WebLockComponentService, + deps: [], + }), safeProvider({ provide: SetPasswordJitService, useClass: WebSetPasswordJitService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index cae73e81595..983067823cb 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,6 +10,7 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -20,6 +21,7 @@ import { RegistrationStartSecondaryComponentData, SetPasswordJitComponent, RegistrationLinkExpiredComponent, + LockV2Component, LockIcon, UserLockIcon, } from "@bitwarden/auth/angular"; @@ -337,21 +339,41 @@ const routes: Routes = [ pageTitle: "logIn", }, }, - { - path: "lock", - canActivate: [deepLinkGuard(), lockGuard()], - children: [ - { - path: "", - component: LockComponent, - }, - ], - data: { - pageTitle: "yourVaultIsLockedV2", - pageIcon: LockIcon, - showReadonlyHostname: true, - } satisfies AnonLayoutWrapperData, - }, + ...extensionRefreshSwap( + LockComponent, + LockV2Component, + { + path: "lock", + canActivate: [deepLinkGuard(), lockGuard()], + children: [ + { + path: "", + component: LockComponent, + }, + ], + data: { + pageTitle: "yourVaultIsLockedV2", + pageIcon: LockIcon, + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + }, + { + path: "lock", + canActivate: [deepLinkGuard(), lockGuard()], + children: [ + { + path: "", + component: LockV2Component, + }, + ], + data: { + pageTitle: "yourAccountIsLocked", + pageIcon: LockIcon, + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + }, + ), + { path: "2fa", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8e847dfb63e..ab43c3af18b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1099,8 +1099,11 @@ "yourVaultIsLockedV2": { "message": "Your vault is locked" }, - "uuid": { - "message": "UUID" + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "uuid":{ + "message" : "UUID" }, "unlock": { "message": "Unlock" @@ -3169,6 +3172,10 @@ "incorrectPin": { "message": "Incorrect PIN" }, + "pin": { + "message": "PIN", + "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." + }, "exportedVault": { "message": "Vault exported" }, @@ -7463,6 +7470,15 @@ "or": { "message": "or" }, + "unlockWithBiometrics": { + "message": "Unlock with biometrics" + }, + "unlockWithPin": { + "message": "Unlock with PIN" + }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "licenseAndBillingManagement": { "message": "License and billing management" }, diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index b54f114d3d4..1486b9b57d8 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -38,6 +38,8 @@ export const authGuard: CanActivateFn = async ( if (routerState != null) { messagingService.send("lockedUrl", { url: routerState.url }); } + // TODO PM-9674: when extension refresh is finished, remove promptBiometric + // as it has been integrated into the component as a default feature. return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } }); } diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index bfb3a67aedc..6de473c33e7 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -43,5 +43,9 @@ export * from "./registration/registration-env-selector/registration-env-selecto export * from "./registration/registration-finish/registration-finish.service"; export * from "./registration/registration-finish/default-registration-finish.service"; +// lock +export * from "./lock/lock.component"; +export * from "./lock/lock-component.service"; + // vault timeout export * from "./vault-timeout-input/vault-timeout-input.component"; diff --git a/libs/auth/src/angular/lock/lock-component.service.ts b/libs/auth/src/angular/lock/lock-component.service.ts new file mode 100644 index 00000000000..fe54db21baa --- /dev/null +++ b/libs/auth/src/angular/lock/lock-component.service.ts @@ -0,0 +1,48 @@ +import { Observable } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +export enum BiometricsDisableReason { + NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem", + EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable", + SystemBiometricsUnavailable = "SystemBiometricsUnavailable", +} + +// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics" +export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption]; + +export const UnlockOption = Object.freeze({ + MasterPassword: "masterPassword", + Pin: "pin", + Biometrics: "biometrics", +}) satisfies { [Prop in keyof UnlockOptions as Capitalize]: Prop }; + +export type UnlockOptions = { + masterPassword: { + enabled: boolean; + }; + pin: { + enabled: boolean; + }; + biometrics: { + enabled: boolean; + disableReason: BiometricsDisableReason | null; + }; +}; + +/** + * The LockComponentService is a service which allows the single libs/auth LockComponent to delegate all + * client specific functionality to client specific services implementations of LockComponentService. + */ +export abstract class LockComponentService { + // Extension + abstract getBiometricsError(error: any): string | null; + abstract getPreviousUrl(): string | null; + + // Desktop only + abstract isWindowVisible(): Promise; + abstract getBiometricsUnlockBtnText(): string; + + // Multi client + abstract getAvailableUnlockOptions$(userId: UserId): Observable; +} diff --git a/libs/auth/src/angular/lock/lock.component.html b/libs/auth/src/angular/lock/lock.component.html new file mode 100644 index 00000000000..5f5991c681e --- /dev/null +++ b/libs/auth/src/angular/lock/lock.component.html @@ -0,0 +1,191 @@ + +
+ +
+
+ + + + + + +
+

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+ + + +
+ + {{ "pin" | i18n }} + + + + +
+ + +

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+
+ + + +
+ + {{ "masterPass" | i18n }} + + + + + + +
+ + +

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+
+
diff --git a/libs/auth/src/angular/lock/lock.component.ts b/libs/auth/src/angular/lock/lock.component.ts new file mode 100644 index 00000000000..7bea14f221e --- /dev/null +++ b/libs/auth/src/angular/lock/lock.component.ts @@ -0,0 +1,638 @@ +import { CommonModule } from "@angular/common"; +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { + MasterPasswordVerification, + MasterPasswordVerificationResponse, +} from "@bitwarden/common/auth/types/verification"; +import { ClientType } from "@bitwarden/common/enums"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { + AsyncActionsModule, + ButtonModule, + DialogService, + FormFieldModule, + IconButtonModule, + ToastService, +} from "@bitwarden/components"; +import { BiometricStateService } from "@bitwarden/key-management"; + +import { PinServiceAbstraction } from "../../common/abstractions"; +import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; + +import { + UnlockOption, + LockComponentService, + UnlockOptions, + UnlockOptionValue, +} from "./lock-component.service"; + +const BroadcasterSubscriptionId = "LockComponent"; + +const clientTypeToSuccessRouteRecord: Partial> = { + [ClientType.Web]: "vault", + [ClientType.Desktop]: "vault", + [ClientType.Browser]: "/tabs/current", +}; + +@Component({ + selector: "bit-lock", + templateUrl: "lock.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + ReactiveFormsModule, + ButtonModule, + FormFieldModule, + AsyncActionsModule, + IconButtonModule, + ], +}) +export class LockV2Component implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + activeAccount: { id: UserId | undefined } & AccountInfo; + + clientType: ClientType; + ClientType = ClientType; + + unlockOptions: UnlockOptions = null; + + UnlockOption = UnlockOption; + + private _activeUnlockOptionBSubject: BehaviorSubject = + new BehaviorSubject(null); + + activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable(); + + set activeUnlockOption(value: UnlockOptionValue) { + this._activeUnlockOptionBSubject.next(value); + } + + get activeUnlockOption(): UnlockOptionValue { + return this._activeUnlockOptionBSubject.value; + } + + private invalidPinAttempts = 0; + + biometricUnlockBtnText: string; + + // masterPassword = ""; + showPassword = false; + private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; + + forcePasswordResetRoute = "update-temp-password"; + + formGroup: FormGroup; + + // Desktop properties: + private deferFocus: boolean = null; + private biometricAsked = false; + + // Browser extension properties: + private isInitialLockScreen = (window as any).previousPopupUrl == null; + + defaultUnlockOptionSetForUser = false; + + unlockingViaBiometrics = false; + + constructor( + private accountService: AccountService, + private pinService: PinServiceAbstraction, + private userVerificationService: UserVerificationService, + private cryptoService: CryptoService, + private platformUtilsService: PlatformUtilsService, + private router: Router, + private dialogService: DialogService, + private messagingService: MessagingService, + private biometricStateService: BiometricStateService, + private ngZone: NgZone, + private i18nService: I18nService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private logService: LogService, + private deviceTrustService: DeviceTrustServiceAbstraction, + private syncService: SyncService, + private policyService: InternalPolicyService, + private passwordStrengthService: PasswordStrengthServiceAbstraction, + private formBuilder: FormBuilder, + private toastService: ToastService, + + private lockComponentService: LockComponentService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + + // desktop deps + private broadcasterService: BroadcasterService, + ) {} + + async ngOnInit() { + this.listenForActiveUnlockOptionChanges(); + + // Listen for active account changes + this.listenForActiveAccountChanges(); + + // Identify client + this.clientType = this.platformUtilsService.getClientType(); + + if (this.clientType === "desktop") { + await this.desktopOnInit(); + } + } + + // Base component methods + private listenForActiveUnlockOptionChanges() { + this.activeUnlockOption$ + .pipe(takeUntil(this.destroy$)) + .subscribe((activeUnlockOption: UnlockOptionValue) => { + if (activeUnlockOption === UnlockOption.Pin) { + this.buildPinForm(); + } else if (activeUnlockOption === UnlockOption.MasterPassword) { + this.buildMasterPasswordForm(); + } + }); + } + + private buildMasterPasswordForm() { + this.formGroup = this.formBuilder.group( + { + masterPassword: ["", [Validators.required]], + }, + { updateOn: "submit" }, + ); + } + + private buildPinForm() { + this.formGroup = this.formBuilder.group( + { + pin: ["", [Validators.required]], + }, + { updateOn: "submit" }, + ); + } + + private listenForActiveAccountChanges() { + this.accountService.activeAccount$ + .pipe( + switchMap((account) => { + return this.handleActiveAccountChange(account); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + private async handleActiveAccountChange(activeAccount: { id: UserId | undefined } & AccountInfo) { + this.activeAccount = activeAccount; + + this.resetDataOnActiveAccountChange(); + + this.setEmailAsPageSubtitle(activeAccount.email); + + this.unlockOptions = await firstValueFrom( + this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id), + ); + + this.setDefaultActiveUnlockOption(this.unlockOptions); + + if (this.unlockOptions.biometrics.enabled) { + await this.handleBiometricsUnlockEnabled(); + } + } + + private resetDataOnActiveAccountChange() { + this.defaultUnlockOptionSetForUser = false; + this.unlockOptions = null; + this.activeUnlockOption = null; + this.formGroup = null; // new form group will be created based on new active unlock option + + // Desktop properties: + this.biometricAsked = false; + } + + private setEmailAsPageSubtitle(email: string) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageSubtitle: { + subtitle: email, + translate: false, + }, + }); + } + + private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions) { + // Priorities should be Biometrics > Pin > Master Password for speed + if (unlockOptions.biometrics.enabled) { + this.activeUnlockOption = UnlockOption.Biometrics; + } else if (unlockOptions.pin.enabled) { + this.activeUnlockOption = UnlockOption.Pin; + } else if (unlockOptions.masterPassword.enabled) { + this.activeUnlockOption = UnlockOption.MasterPassword; + } + } + + private async handleBiometricsUnlockEnabled() { + this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText(); + + const autoPromptBiometrics = await firstValueFrom( + this.biometricStateService.promptAutomatically$, + ); + + // TODO: PM-12546 - we need to make our biometric autoprompt experience consistent between the + // desktop and extension. + if (this.clientType === "desktop") { + if (autoPromptBiometrics) { + await this.desktopAutoPromptBiometrics(); + } + } + + if (this.clientType === "browser") { + if ( + this.unlockOptions.biometrics.enabled && + autoPromptBiometrics && + this.isInitialLockScreen // only autoprompt biometrics on initial lock screen + ) { + await this.unlockViaBiometrics(); + } + } + } + + // Note: this submit method is only used for unlock methods that require a form and user input. + // For biometrics unlock, the method is called directly. + submit = async (): Promise => { + if (this.activeUnlockOption === UnlockOption.Pin) { + return await this.unlockViaPin(); + } + + await this.unlockViaMasterPassword(); + }; + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + if (confirmed) { + this.messagingService.send("logout", { userId: this.activeAccount.id }); + } + } + + async unlockViaBiometrics(): Promise { + this.unlockingViaBiometrics = true; + + if (!this.unlockOptions.biometrics.enabled) { + this.unlockingViaBiometrics = false; + return; + } + + try { + await this.biometricStateService.setUserPromptCancelled(); + const userKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Biometric, + this.activeAccount.id, + ); + + // If user cancels biometric prompt, userKey is undefined. + if (userKey) { + await this.setUserKeyAndContinue(userKey, false); + } + + this.unlockingViaBiometrics = false; + } catch (e) { + // Cancelling is a valid action. + if (e?.message === "canceled") { + this.unlockingViaBiometrics = false; + return; + } + + let biometricTranslatedErrorDesc; + + if (this.clientType === "browser") { + const biometricErrorDescTranslationKey = this.lockComponentService.getBiometricsError(e); + + if (biometricErrorDescTranslationKey) { + biometricTranslatedErrorDesc = this.i18nService.t(biometricErrorDescTranslationKey); + } + } + + // if no translation key found, show generic error message + if (!biometricTranslatedErrorDesc) { + biometricTranslatedErrorDesc = this.i18nService.t("unexpectedError"); + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "error" }, + content: biometricTranslatedErrorDesc, + acceptButtonText: { key: "tryAgain" }, + type: "danger", + }); + + if (confirmed) { + // try again + await this.unlockViaBiometrics(); + } + + this.unlockingViaBiometrics = false; + } + } + + togglePassword() { + this.showPassword = !this.showPassword; + const input = document.getElementById( + this.unlockOptions.pin.enabled ? "pin" : "masterPassword", + ); + if (this.ngZone.isStable) { + input.focus(); + } else { + // eslint-disable-next-line rxjs-angular/prefer-takeuntil + this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus()); + } + } + + private validatePin(): boolean { + if (this.formGroup.invalid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("pinRequired"), + }); + return false; + } + + return true; + } + + private async unlockViaPin() { + if (!this.validatePin()) { + return; + } + + const pin = this.formGroup.controls.pin.value; + + const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5; + + try { + const userKey = await this.pinService.decryptUserKeyWithPin(pin, this.activeAccount.id); + + if (userKey) { + await this.setUserKeyAndContinue(userKey); + return; // successfully unlocked + } + + // Failure state: invalid PIN or failed decryption + this.invalidPinAttempts++; + + // Log user out if they have entered an invalid PIN too many times + if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), + }); + this.messagingService.send("logout"); + return; + } + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidPin"), + }); + } catch { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); + } + } + + private validateMasterPassword(): boolean { + if (this.formGroup.invalid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); + return false; + } + + return true; + } + + private async unlockViaMasterPassword() { + if (!this.validateMasterPassword()) { + return; + } + + const masterPassword = this.formGroup.controls.masterPassword.value; + + const verification = { + type: VerificationType.MasterPassword, + secret: masterPassword, + } as MasterPasswordVerification; + + let passwordValid = false; + let masterPasswordVerificationResponse: MasterPasswordVerificationResponse; + try { + masterPasswordVerificationResponse = + await this.userVerificationService.verifyUserByMasterPassword( + verification, + this.activeAccount.id, + this.activeAccount.email, + ); + + this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse( + masterPasswordVerificationResponse.policyOptions, + ); + passwordValid = true; + } catch (e) { + this.logService.error(e); + } + + if (!passwordValid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); + return; + } + + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( + masterPasswordVerificationResponse.masterKey, + ); + await this.setUserKeyAndContinue(userKey, true); + } + + private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) { + await this.cryptoService.setUserKey(key, this.activeAccount.id); + + // Now that we have a decrypted user key in memory, we can check if we + // need to establish trust on the current device + await this.deviceTrustService.trustDeviceIfRequired(this.activeAccount.id); + + await this.doContinue(evaluatePasswordAfterUnlock); + } + + private async doContinue(evaluatePasswordAfterUnlock: boolean) { + await this.biometricStateService.resetUserPromptCancelled(); + this.messagingService.send("unlocked"); + + if (evaluatePasswordAfterUnlock) { + try { + // If we do not have any saved policies, attempt to load them from the service + if (this.enforcedMasterPasswordOptions == undefined) { + this.enforcedMasterPasswordOptions = await firstValueFrom( + this.policyService.masterPasswordPolicyOptions$(), + ); + } + + if (this.requirePasswordChange()) { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + userId, + ); + await this.router.navigate([this.forcePasswordResetRoute]); + return; + } + } catch (e) { + // Do not prevent unlock if there is an error evaluating policies + this.logService.error(e); + } + } + + // Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service. + await this.syncService.fullSync(false); + + if (this.clientType === "browser") { + const previousUrl = this.lockComponentService.getPreviousUrl(); + if (previousUrl) { + await this.router.navigateByUrl(previousUrl); + } + } + + // determine success route based on client type + const successRoute = clientTypeToSuccessRouteRecord[this.clientType]; + await this.router.navigate([successRoute]); + } + + /** + * Checks if the master password meets the enforced policy requirements + * If not, returns false + */ + private requirePasswordChange(): boolean { + if ( + this.enforcedMasterPasswordOptions == undefined || + !this.enforcedMasterPasswordOptions.enforceOnLogin + ) { + return false; + } + + const masterPassword = this.formGroup.controls.masterPassword.value; + + const passwordStrength = this.passwordStrengthService.getPasswordStrength( + masterPassword, + this.activeAccount.email, + )?.score; + + return !this.policyService.evaluateMasterPassword( + passwordStrength, + masterPassword, + this.enforcedMasterPasswordOptions, + ); + } + + // ----------------------------------------------------------------------------------------------- + // Desktop methods: + // ----------------------------------------------------------------------------------------------- + + async desktopOnInit() { + // TODO: move this into a WindowService and subscribe to messages via MessageListener service. + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(() => { + switch (message.command) { + case "windowHidden": + this.onWindowHidden(); + break; + case "windowIsFocused": + if (this.deferFocus === null) { + this.deferFocus = !message.windowIsFocused; + if (!this.deferFocus) { + this.focusInput(); + } + } else if (this.deferFocus && message.windowIsFocused) { + this.focusInput(); + this.deferFocus = false; + } + break; + default: + } + }); + }); + this.messagingService.send("getWindowIsFocused"); + } + + private async desktopAutoPromptBiometrics() { + if (!this.unlockOptions?.biometrics?.enabled || this.biometricAsked) { + return; + } + + // prevent the biometric prompt from showing if the user has already cancelled it + if (await firstValueFrom(this.biometricStateService.promptCancelled$)) { + return; + } + + const windowVisible = await this.lockComponentService.isWindowVisible(); + + if (windowVisible) { + this.biometricAsked = true; + await this.unlockViaBiometrics(); + } + } + + onWindowHidden() { + this.showPassword = false; + } + + private focusInput() { + if (this.unlockOptions) { + document.getElementById(this.unlockOptions.pin.enabled ? "pin" : "masterPassword")?.focus(); + } + } + + // ----------------------------------------------------------------------------------------------- + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + + if (this.clientType === "desktop") { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + } +} From 97a97c4b2d8183dde02391d5f0aa8de8617b15aa Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:38:27 -0700 Subject: [PATCH 20/25] disable copy button if no password is present (#11349) --- .../src/send-form/components/options/send-options.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index dbd86d7f5b7..adbca181947 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -35,6 +35,7 @@ bitIconButton="bwi-clone" bitSuffix [appA11yTitle]="'copyPassword' | i18n" + [disabled]="!sendOptionsForm.get('password').value" [valueLabel]="'password' | i18n" [appCopyClick]="sendOptionsForm.get('password').value" showToast From 8b034cda7db8cb89014ffcbeaf6891733eb0dff6 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:01:01 +0100 Subject: [PATCH 21/25] Remove the delete provider flag (#11336) --- .../admin-console/providers/settings/account.component.html | 2 +- .../app/admin-console/providers/settings/account.component.ts | 4 ---- libs/common/src/enums/feature-flag.enum.ts | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html index a4e45877552..b6794b2987f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html @@ -34,7 +34,7 @@ - + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index d5d7634db4c..0442f04fb72 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -8,7 +8,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -33,9 +32,6 @@ export class AccountComponent implements OnDestroy, OnInit { providerName: ["" as ProviderResponse["name"]], providerBillingEmail: ["" as ProviderResponse["billingEmail"], Validators.email], }); - protected enableDeleteProvider$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableDeleteProvider, - ); constructor( private apiService: ApiService, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 2b7d2bea334..f8967212b20 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -9,7 +9,6 @@ export enum FeatureFlag { GeneratorToolsModernization = "generator-tools-modernization", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", - EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", @@ -54,7 +53,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.GeneratorToolsModernization]: FALSE, [FeatureFlag.EnableConsolidatedBilling]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, - [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, From 363acf58f97e9ce4c8682ead94ac5b8269f7e304 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:07:13 +1000 Subject: [PATCH 22/25] [PM-12740] Move CollectionAdminService to AC Team (#11269) --- .../organizations/core/views/group.view.ts | 3 +-- .../organizations/core/views/index.ts | 1 - .../core/views/organization-user-admin-view.ts | 3 +-- .../core/views/organization-user.view.ts | 7 ++++--- .../manage/group-add-edit.component.ts | 8 +++++--- .../member-dialog/member-dialog.component.ts | 12 ++++++------ .../settings/org-import.component.ts | 2 +- .../access-selector/access-selector.models.ts | 7 +++++-- apps/web/src/app/core/core.module.ts | 14 +++++++++++--- .../import/import-collection-admin.service.ts | 4 ++-- .../collection-dialog.component.ts | 11 ++++------- .../vault-collection-row.component.ts | 3 +-- .../vault-items/vault-items.component.ts | 2 +- .../vault-items/vault-items.stories.ts | 12 ++++++------ .../organization-name-badge.component.ts | 3 +-- .../abstractions/vault-filter.service.ts | 2 +- .../routed-vault-filter-bridge.service.ts | 7 ++----- .../services/vault-filter.service.ts | 2 +- .../shared/models/filter-function.spec.ts | 3 ++- .../shared/models/filter-function.ts | 3 ++- .../models/routed-vault-filter-bridge.model.ts | 2 +- .../shared/models/routed-vault-filter.model.ts | 2 -- .../shared/models/vault-filter.type.ts | 3 +-- .../vault-header/vault-header.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 2 +- .../bulk-collections-dialog.component.ts | 6 ++++-- .../vault-filter/vault-filter.service.ts | 2 +- .../vault-header/vault-header.component.ts | 8 +++++--- .../src/app/vault/org-vault/vault.component.ts | 8 +++++--- apps/web/src/app/vault/utils/collection-utils.ts | 3 +-- .../services/member-access-report.service.ts | 2 +- .../abstractions/collection-admin.service.ts | 16 ++++++++++++++++ .../src/common/collections/abstractions/index.ts | 1 + .../src/common/collections/index.ts | 3 +++ .../models}/bulk-collection-access.request.ts | 0 .../models}/collection-access-selection.view.ts | 0 .../collections/models}/collection-admin.view.ts | 5 +++-- .../src/common/collections/models/index.ts | 3 +++ .../services/default-collection-admin.service.ts | 15 +++++++-------- .../src/common/collections/services/index.ts | 1 + libs/admin-console/src/common/index.ts | 1 + 41 files changed, 113 insertions(+), 81 deletions(-) create mode 100644 libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts create mode 100644 libs/admin-console/src/common/collections/abstractions/index.ts create mode 100644 libs/admin-console/src/common/collections/index.ts rename {apps/web/src/app/vault/core => libs/admin-console/src/common/collections/models}/bulk-collection-access.request.ts (100%) rename {apps/web/src/app/admin-console/organizations/core/views => libs/admin-console/src/common/collections/models}/collection-access-selection.view.ts (100%) rename {apps/web/src/app/vault/core/views => libs/admin-console/src/common/collections/models}/collection-admin.view.ts (92%) create mode 100644 libs/admin-console/src/common/collections/models/index.ts rename apps/web/src/app/vault/core/collection-admin.service.ts => libs/admin-console/src/common/collections/services/default-collection-admin.service.ts (94%) create mode 100644 libs/admin-console/src/common/collections/services/index.ts diff --git a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts index 1909b9a863c..67ce47c624a 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts @@ -1,9 +1,8 @@ +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { View } from "@bitwarden/common/src/models/view/view"; import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response"; -import { CollectionAccessSelectionView } from "./collection-access-selection.view"; - export class GroupView implements View { id: string; organizationId: string; diff --git a/apps/web/src/app/admin-console/organizations/core/views/index.ts b/apps/web/src/app/admin-console/organizations/core/views/index.ts index ef14753c48a..9408d7757c3 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/index.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/index.ts @@ -1,4 +1,3 @@ -export * from "./collection-access-selection.view"; export * from "./group.view"; export * from "./organization-user.view"; export * from "./organization-user-admin-view"; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts index 97e77d8543c..b9b034b405d 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts @@ -1,11 +1,10 @@ +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; -import { CollectionAccessSelectionView } from "./collection-access-selection.view"; - export class OrganizationUserAdminView { id: string; userId: string; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts index 8988f41487c..7d1a10c5332 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts @@ -1,12 +1,13 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; +import { + OrganizationUserUserDetailsResponse, + CollectionAccessSelectionView, +} from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; -import { CollectionAccessSelectionView } from "./collection-access-selection.view"; - export class OrganizationUserView { id: string; userId: string; diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index cdbc049111d..643e76e4c38 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -14,7 +14,11 @@ import { takeUntil, } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAdminService, + CollectionAdminView, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -26,8 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { UserId } from "@bitwarden/common/types/guid"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CollectionAdminService } from "../../../vault/core/collection-admin.service"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { InternalGroupService as GroupService, GroupView } from "../core"; import { AccessItemType, diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index fb11ad21c4c..aac096189e0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -13,7 +13,12 @@ import { takeUntil, } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAccessSelectionView, + CollectionAdminService, + CollectionAdminView, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType, @@ -24,14 +29,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; -import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view"; import { - CollectionAccessSelectionView, GroupService, GroupView, OrganizationUserAdminView, @@ -133,7 +134,6 @@ export class MemberDialogComponent implements OnDestroy { @Inject(DIALOG_DATA) protected params: MemberDialogParams, private dialogRef: DialogRef, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, // TODO: We should really look into consolidating naming conventions for these services private collectionAdminService: CollectionAdminService, diff --git a/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts b/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts index 36cfd4230c7..2c2d700fe80 100644 --- a/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; +import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { canAccessVaultTab, OrganizationService, @@ -11,7 +12,6 @@ import { ImportComponent } from "@bitwarden/importer/ui"; import { LooseComponentsModule, SharedModule } from "../../../shared"; import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service"; -import { CollectionAdminService } from "../../../vault/core/collection-admin.service"; @Component({ templateUrl: "org-import.component.html", diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index 429b62ed0cc..1dc20366942 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -1,11 +1,14 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; +import { + CollectionAccessSelectionView, + OrganizationUserUserDetailsResponse, +} from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { SelectItemView } from "@bitwarden/components"; -import { CollectionAccessSelectionView, GroupView } from "../../../core"; +import { GroupView } from "../../../core"; /** * Permission options that replace/correspond with manage, readOnly, and hidePassword server fields. diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index c14c9750474..37ce80a826d 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -1,7 +1,11 @@ import { CommonModule } from "@angular/common"; import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAdminService, + DefaultCollectionAdminService, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { CLIENT_TYPE, @@ -60,6 +64,7 @@ import { ThemeStateService, } from "@bitwarden/common/platform/theming/theme-state.service"; import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { BiometricsService } from "@bitwarden/key-management"; import { PolicyListService } from "../admin-console/core/policy-list.service"; @@ -75,7 +80,6 @@ import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; -import { CollectionAdminService } from "../vault/core/collection-admin.service"; import { EventService } from "./event.service"; import { InitService } from "./init.service"; @@ -149,7 +153,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebFileDownloadService, useAngularDecorators: true, }), - safeProvider(CollectionAdminService), safeProvider({ provide: WindowStorageService, useFactory: () => new WindowStorageService(window.localStorage), @@ -227,6 +230,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultAppIdService, deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService], }), + safeProvider({ + provide: CollectionAdminService, + useClass: DefaultCollectionAdminService, + deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService], + }), ]; @NgModule({ diff --git a/apps/web/src/app/tools/import/import-collection-admin.service.ts b/apps/web/src/app/tools/import/import-collection-admin.service.ts index e48f9b27ce6..093bfed7024 100644 --- a/apps/web/src/app/tools/import/import-collection-admin.service.ts +++ b/apps/web/src/app/tools/import/import-collection-admin.service.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; +import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; + import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction"; -import { CollectionAdminService } from "../../vault/core/collection-admin.service"; -import { CollectionAdminView } from "../../vault/core/views/collection-admin.view"; @Injectable() export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction { diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 5c46a7a0296..821765ba41b 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -14,6 +14,9 @@ import { import { first } from "rxjs/operators"; import { + CollectionAccessSelectionView, + CollectionAdminService, + CollectionAdminView, OrganizationUserApiService, OrganizationUserUserMiniResponse, } from "@bitwarden/admin-console/common"; @@ -26,11 +29,7 @@ import { CollectionResponse } from "@bitwarden/common/vault/models/response/coll import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { BitValidators, DialogService } from "@bitwarden/components"; -import { - CollectionAccessSelectionView, - GroupService, - GroupView, -} from "../../../admin-console/organizations/core"; +import { GroupService, GroupView } from "../../../admin-console/organizations/core"; import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component"; import { AccessItemType, @@ -40,8 +39,6 @@ import { convertToPermission, convertToSelectionView, } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models"; -import { CollectionAdminService } from "../../core/collection-admin.service"; -import { CollectionAdminView } from "../../core/views/collection-admin.view"; export enum CollectionDialogTabType { Info = 0, diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 09e7484b673..ec38c53480a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -1,12 +1,11 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { CollectionAdminView, Unassigned } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { GroupView } from "../../../admin-console/organizations/core"; -import { CollectionAdminView } from "../../core/views/collection-admin.view"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { convertToPermission, diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 6ac19b75655..591c132e1e6 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -1,13 +1,13 @@ import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { TableDataSource } from "@bitwarden/components"; import { GroupView } from "../../../admin-console/organizations/core"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { VaultItem } from "./vault-item"; import { VaultItemEvent } from "./vault-item-event"; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index bf0fa3aaef4..96089d2b156 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -3,6 +3,11 @@ import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { BehaviorSubject, of } from "rxjs"; +import { + CollectionAccessSelectionView, + CollectionAdminView, + Unassigned, +} from "@bitwarden/admin-console/common"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -19,13 +24,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { - CollectionAccessSelectionView, - GroupView, -} from "../../../admin-console/organizations/core"; +import { GroupView } from "../../../admin-console/organizations/core"; import { PreloadedEnglishI18nModule } from "../../../core/tests"; -import { CollectionAdminView } from "../../core/views/collection-admin.view"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { VaultItemsComponent } from "./vault-items.component"; import { VaultItemsModule } from "./vault-items.module"; diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index 6d53b8ad720..3e37d4998de 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -1,13 +1,12 @@ import { Component, Input, OnChanges } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { Unassigned } from "../vault-filter/shared/models/routed-vault-filter.model"; - @Component({ selector: "app-org-badge", templateUrl: "organization-name-badge.component.html", diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index 836cba22016..b18ee76e9c8 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -1,11 +1,11 @@ import { Observable } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CollectionView } from "@bitwarden/common/src/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../../core/views/collection-admin.view"; import { CipherTypeFilter, CollectionFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts index 08ce9b67ba5..1f0a9e135b5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts @@ -2,15 +2,12 @@ import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; import { combineLatest, map, Observable } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model"; -import { - RoutedVaultFilterModel, - Unassigned, - All, -} from "../shared/models/routed-vault-filter.model"; +import { RoutedVaultFilterModel, All } from "../shared/models/routed-vault-filter.model"; import { VaultFilter } from "../shared/models/vault-filter.model"; import { CipherTypeFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index ac20f86d0ee..d8abfb2f794 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -10,6 +10,7 @@ import { switchMap, } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -26,7 +27,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state"; -import { CollectionAdminView } from "../../../core/views/collection-admin.view"; import { CipherTypeFilter, CollectionFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts index 786b2b1c7aa..397b7810606 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts @@ -1,8 +1,9 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { createFilterFunction } from "./filter-function"; -import { Unassigned, All } from "./routed-vault-filter.model"; +import { All } from "./routed-vault-filter.model"; describe("createFilter", () => { describe("given a generic cipher", () => { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts index 4716eb631b1..4b038512581 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -1,7 +1,8 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { All, RoutedVaultFilterModel, Unassigned } from "./routed-vault-filter.model"; +import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model"; export type FilterFunction = (cipher: CipherView) => boolean; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts index 2f6047b6bbc..fe236a089e0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts @@ -1,3 +1,4 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -8,7 +9,6 @@ import { isRoutedVaultFilterItemType, RoutedVaultFilterItemType, RoutedVaultFilterModel, - Unassigned, } from "./routed-vault-filter.model"; import { VaultFilter, VaultFilterFunction } from "./vault-filter.model"; import { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts index 5579c62d4e9..4f2659d6101 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts @@ -1,5 +1,3 @@ -export const Unassigned = "unassigned"; - export const All = "all"; // TODO: Remove `All` when moving to vertical navigation. diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts index fd349069aaa..0cd385bd19d 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts @@ -1,10 +1,9 @@ +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../../core/views/collection-admin.view"; - export type CipherStatus = "all" | "favorites" | "trash" | CipherType; export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string }; diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 44e523abe61..463a03091e0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -9,6 +9,7 @@ import { } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -26,7 +27,6 @@ import { PipesModule } from "../pipes/pipes.module"; import { All, RoutedVaultFilterModel, - Unassigned, } from "../vault-filter/shared/models/routed-vault-filter.model"; @Component({ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 8ad9deaf2bc..c94294d37d7 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -28,6 +28,7 @@ import { tap, } from "rxjs/operators"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -118,7 +119,6 @@ import { createFilterFunction } from "./vault-filter/shared/models/filter-functi import { All, RoutedVaultFilterModel, - Unassigned, } from "./vault-filter/shared/models/routed-vault-filter.model"; import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model"; import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type"; diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index c4b0d8bc2a2..2839a6ae607 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -3,7 +3,10 @@ import { Component, Inject, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAdminService, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; 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"; @@ -23,7 +26,6 @@ import { PermissionMode, } from "../../../admin-console/organizations/shared/components/access-selector"; import { SharedModule } from "../../../shared"; -import { CollectionAdminService } from "../../core/collection-admin.service"; export interface BulkCollectionsDialogParams { organizationId: string; diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts index c6d4ee590b8..f9717f19f1e 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts @@ -1,6 +1,7 @@ import { Injectable, OnDestroy } from "@angular/core"; import { map, Observable, ReplaySubject, Subject } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -10,7 +11,6 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { VaultFilterService as BaseVaultFilterService } from "../../individual-vault/vault-filter/services/vault-filter.service"; import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type"; diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 429062917ad..a3d564a9a3a 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -3,6 +3,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; +import { + CollectionAdminService, + CollectionAdminView, + Unassigned, +} from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -22,13 +27,10 @@ import { import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { CollectionDialogTabType } from "../../components/collection-dialog"; -import { CollectionAdminService } from "../../core/collection-admin.service"; import { All, RoutedVaultFilterModel, - Unassigned, } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; @Component({ diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 3120b54ed38..7118fea4d09 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -30,6 +30,11 @@ import { withLatestFrom, } from "rxjs/operators"; +import { + CollectionAdminService, + CollectionAdminView, + Unassigned, +} from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -72,8 +77,6 @@ import { } from "../components/collection-dialog"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; -import { CollectionAdminService } from "../core/collection-admin.service"; -import { CollectionAdminView } from "../core/views/collection-admin.view"; import { BulkDeleteDialogResult, openBulkDeleteDialog, @@ -85,7 +88,6 @@ import { createFilterFunction } from "../individual-vault/vault-filter/shared/mo import { All, RoutedVaultFilterModel, - Unassigned, } from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { openViewCipherDialog, diff --git a/apps/web/src/app/vault/utils/collection-utils.ts b/apps/web/src/app/vault/utils/collection-utils.ts index b035c40f9f5..2f93e46bed2 100644 --- a/apps/web/src/app/vault/utils/collection-utils.ts +++ b/apps/web/src/app/vault/utils/collection-utils.ts @@ -1,3 +1,4 @@ +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CollectionView, @@ -5,8 +6,6 @@ import { } from "@bitwarden/common/vault/models/view/collection.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { CollectionAdminView } from "../../vault/core/views/collection-admin.view"; - export function getNestedCollectionTree( collections: CollectionAdminView[], ): TreeNode[]; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index 3616893e231..443edc1d2fc 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -1,9 +1,9 @@ import { Injectable } from "@angular/core"; +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { CollectionAccessSelectionView } from "@bitwarden/web-vault/app/admin-console/organizations/core/views"; import { getPermissionList, convertToPermission, diff --git a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts new file mode 100644 index 00000000000..e5b0bde7ef6 --- /dev/null +++ b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts @@ -0,0 +1,16 @@ +import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; + +import { CollectionAccessSelectionView, CollectionAdminView } from "../models"; + +export abstract class CollectionAdminService { + getAll: (organizationId: string) => Promise; + get: (organizationId: string, collectionId: string) => Promise; + save: (collection: CollectionAdminView) => Promise; + delete: (organizationId: string, collectionId: string) => Promise; + bulkAssignAccess: ( + organizationId: string, + collectionIds: string[], + users: CollectionAccessSelectionView[], + groups: CollectionAccessSelectionView[], + ) => Promise; +} diff --git a/libs/admin-console/src/common/collections/abstractions/index.ts b/libs/admin-console/src/common/collections/abstractions/index.ts new file mode 100644 index 00000000000..4ee56102061 --- /dev/null +++ b/libs/admin-console/src/common/collections/abstractions/index.ts @@ -0,0 +1 @@ +export * from "./collection-admin.service"; diff --git a/libs/admin-console/src/common/collections/index.ts b/libs/admin-console/src/common/collections/index.ts new file mode 100644 index 00000000000..9187ccd39cf --- /dev/null +++ b/libs/admin-console/src/common/collections/index.ts @@ -0,0 +1,3 @@ +export * from "./abstractions"; +export * from "./models"; +export * from "./services"; diff --git a/apps/web/src/app/vault/core/bulk-collection-access.request.ts b/libs/admin-console/src/common/collections/models/bulk-collection-access.request.ts similarity index 100% rename from apps/web/src/app/vault/core/bulk-collection-access.request.ts rename to libs/admin-console/src/common/collections/models/bulk-collection-access.request.ts diff --git a/apps/web/src/app/admin-console/organizations/core/views/collection-access-selection.view.ts b/libs/admin-console/src/common/collections/models/collection-access-selection.view.ts similarity index 100% rename from apps/web/src/app/admin-console/organizations/core/views/collection-access-selection.view.ts rename to libs/admin-console/src/common/collections/models/collection-access-selection.view.ts diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/libs/admin-console/src/common/collections/models/collection-admin.view.ts similarity index 92% rename from apps/web/src/app/vault/core/views/collection-admin.view.ts rename to libs/admin-console/src/common/collections/models/collection-admin.view.ts index 10f894505c9..208131a3f71 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/libs/admin-console/src/common/collections/models/collection-admin.view.ts @@ -2,8 +2,9 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; +import { CollectionAccessSelectionView } from "../models"; + +export const Unassigned = "unassigned"; export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; diff --git a/libs/admin-console/src/common/collections/models/index.ts b/libs/admin-console/src/common/collections/models/index.ts new file mode 100644 index 00000000000..4f35728b00a --- /dev/null +++ b/libs/admin-console/src/common/collections/models/index.ts @@ -0,0 +1,3 @@ +export * from "./bulk-collection-access.request"; +export * from "./collection-access-selection.view"; +export * from "./collection-admin.view"; diff --git a/apps/web/src/app/vault/core/collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts similarity index 94% rename from apps/web/src/app/vault/core/collection-admin.service.ts rename to libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index e0c15e34047..aa2b5bb91d6 100644 --- a/apps/web/src/app/vault/core/collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -1,5 +1,3 @@ -import { Injectable } from "@angular/core"; - import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -14,13 +12,14 @@ import { CollectionResponse, } from "@bitwarden/common/vault/models/response/collection.response"; -import { CollectionAccessSelectionView } from "../../admin-console/organizations/core"; +import { CollectionAdminService } from "../abstractions"; +import { + BulkCollectionAccessRequest, + CollectionAccessSelectionView, + CollectionAdminView, +} from "../models"; -import { BulkCollectionAccessRequest } from "./bulk-collection-access.request"; -import { CollectionAdminView } from "./views/collection-admin.view"; - -@Injectable() -export class CollectionAdminService { +export class DefaultCollectionAdminService implements CollectionAdminService { constructor( private apiService: ApiService, private cryptoService: CryptoService, diff --git a/libs/admin-console/src/common/collections/services/index.ts b/libs/admin-console/src/common/collections/services/index.ts new file mode 100644 index 00000000000..1e3ed96c6a0 --- /dev/null +++ b/libs/admin-console/src/common/collections/services/index.ts @@ -0,0 +1 @@ +export * from "./default-collection-admin.service"; diff --git a/libs/admin-console/src/common/index.ts b/libs/admin-console/src/common/index.ts index 0af54f8ffbf..edeff5aa314 100644 --- a/libs/admin-console/src/common/index.ts +++ b/libs/admin-console/src/common/index.ts @@ -1 +1,2 @@ export * from "./organization-user"; +export * from "./collections"; From 136776571290c20d384fcad2bdaef3731cbdf44c Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 2 Oct 2024 11:23:40 +0200 Subject: [PATCH 23/25] [PM-11503] Organization Automatic Sync verbiage is misleading (#11151) --- .../billing-sync-api-key.component.html | 2 +- .../billing-sync-key.component.html | 2 +- ...nization-subscription-cloud.component.html | 2 +- ...ation-subscription-selfhost.component.html | 6 ++--- apps/web/src/locales/en/messages.json | 23 +++++++++++-------- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html index 8b5ef867cca..4857a43a1ca 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html @@ -1,7 +1,7 @@

- {{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }} + {{ (hasBillingToken ? "viewBillingToken" : "generateBillingToken") | i18n }}

diff --git a/apps/web/src/app/billing/organizations/billing-sync-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-key.component.html index 808cd83ec67..5f6b8482875 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-key.component.html @@ -1,7 +1,7 @@

- {{ "manageBillingSync" | i18n }} + {{ "manageBillingTokenSync" | i18n }}

{{ "billingSyncKeyDesc" | i18n }}

diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 341324c4a2a..643eeb93bad 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -280,7 +280,7 @@ (click)="manageBillingSync()" *ngIf="canManageBillingSync" > - {{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }} + {{ (hasBillingSyncToken ? "viewBillingToken" : "setUpBillingSync") | i18n }}
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index 0a029de79dc..5a1ccc0768a 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -90,7 +90,7 @@ - {{ "billingSyncDesc" | i18n }} + {{ "automaticBillingSyncDesc" | i18n }} @@ -100,7 +100,7 @@ type="button" (click)="manageBillingSyncSelfHosted()" > - {{ "manageBillingSync" | i18n }} + {{ "manageBillingTokenSync" | i18n }}