From 2ae6fbe2754f96e6e3ee19023ba96cd5839e32ef Mon Sep 17 00:00:00 2001 From: Alex Urbina <42731074+urbinaalex17@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:05:25 -0600 Subject: [PATCH 01/51] DEVOPS-1843 Fix US DEV Web Vault deploys one commit behind (#8458) * DEVOPS-1843 REFACTOR: Trigger web vault deploy step to send the build-web run-id to deploy-web workflow * DEVOPS-1843 ADD: build-web-run-id input to deploy-web workflow to download specific run_id artifact * DEVOPS-1843 FIX: build-web-run-id input in build-web workflow * DEVOPS-1843 REFACTOR: build-web-run-id parameter type to number * DEVOPS-1843 ADD: build-web-run-id input to deploy-web workflow to workflow_dispatch * DEVOPS-1843 FIX: build-web-run-id type in deploy-web.yml * DEVOPS-1843 REFACTOR: web vault deploy action to use GitHub Run ID * DEVOPS-1843 REFACTOR: cloud asset download steps in deploy-web.yml * DEVOPS-1843 REFACTOR: description for build-web workflow Run ID Co-authored-by: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> --------- Co-authored-by: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> --- .github/workflows/build-web.yml | 4 ++-- .github/workflows/deploy-web.yml | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index abd253877368..8576fb6760a9 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -299,7 +299,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - name: Trigger web vault deploy + - name: Trigger web vault deploy using GitHub Run ID uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -311,7 +311,7 @@ jobs: ref: 'main', inputs: { 'environment': 'USDEV', - 'branch-or-tag': 'main' + 'build-web-run-id': '${{ github.run_id }}' } }) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 2d784652a571..769e70058811 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -27,6 +27,10 @@ on: description: "Debug mode" type: boolean default: true + build-web-run-id: + description: "Build-web workflow Run ID to use for artifact download" + type: string + required: false workflow_call: inputs: @@ -46,6 +50,10 @@ on: description: "Debug mode" type: boolean default: true + build-web-run-id: + description: "Build-web workflow Run ID to use for artifact download" + type: string + required: false permissions: deployments: write @@ -168,7 +176,20 @@ jobs: env: _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} steps: + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' + if: ${{ inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' + if: ${{ !inputs.build-web-run-id }} uses: bitwarden/gh-actions/download-artifacts@main id: download-artifacts continue-on-error: true @@ -249,7 +270,20 @@ jobs: keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant" + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' + if: ${{ inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + - name: 'Download cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' + if: ${{ !inputs.build-web-run-id }} uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-web.yml From b180463cc0f5f0603cb5388dd7ec2c838719de2b Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:10:40 +0000 Subject: [PATCH 02/51] Changed logic to validate if cipher has a collection on targetSelector method (#8422) Added tests to import.service.spec.ts that test if the collectionRelationship and folderRelationship properly adapts --- .../cipher-with-collections.json.ts | 37 ++++++++++++ .../src/services/import.service.spec.ts | 56 +++++++++++++++++++ libs/importer/src/services/import.service.ts | 6 +- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts diff --git a/libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts b/libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts new file mode 100644 index 000000000000..ef92ea798498 --- /dev/null +++ b/libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts @@ -0,0 +1,37 @@ +export const cipherWithCollections = `{ + "encrypted": false, + "collections": [ + { + "id": "8e3f5ba1-3e87-4ee8-8da9-b1180099ff9f", + "organizationId": "c6181652-66eb-4cd9-a7f2-b02a00e12352", + "name": "asdf", + "externalId": null + } + ], + "items": [ + { + "passwordHistory": null, + "revisionDate": "2024-02-16T09:20:48.383Z", + "creationDate": "2024-02-16T09:20:48.383Z", + "deletedDate": null, + "id": "f761a968-4b0f-4090-a568-b118009a07b5", + "organizationId": "c6181652-66eb-4cd9-a7f2-b02a00e12352", + "folderId": null, + "type": 1, + "reprompt": 0, + "name": "asdf123", + "notes": null, + "favorite": false, + "login": { + "fido2Credentials": [], + "uris": [], + "username": null, + "password": null, + "totp": null + }, + "collectionIds": [ + "8e3f5ba1-3e87-4ee8-8da9-b1180099ff9f" + ] + } + ] + }`; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index a95b74d792ce..eb21f384b561 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -6,6 +6,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -207,5 +208,60 @@ describe("ImportService", () => { await expect(setImportTargetMethod).rejects.toThrow("Error assigning target folder"); }); + + it("passing importTarget, collectionRelationship has the expected values", async () => { + collectionService.getAllDecrypted.mockResolvedValue([ + mockImportTargetCollection, + mockCollection1, + mockCollection2, + ]); + + importResult.ciphers.push(createCipher({ name: "cipher1" })); + importResult.ciphers.push(createCipher({ name: "cipher2" })); + importResult.collectionRelationships.push([0, 0]); + importResult.collections.push(mockCollection1); + importResult.collections.push(mockCollection2); + + await importService["setImportTarget"]( + importResult, + organizationId, + mockImportTargetCollection, + ); + expect(importResult.collectionRelationships.length).toEqual(2); + expect(importResult.collectionRelationships[0]).toEqual([1, 0]); + expect(importResult.collectionRelationships[1]).toEqual([0, 1]); + }); + + it("passing importTarget, folderRelationship has the expected values", async () => { + folderService.getAllDecryptedFromState.mockResolvedValue([ + mockImportTargetFolder, + mockFolder1, + mockFolder2, + ]); + + importResult.folders.push(mockFolder1); + importResult.folders.push(mockFolder2); + + importResult.ciphers.push(createCipher({ name: "cipher1", folderId: mockFolder1.id })); + importResult.ciphers.push(createCipher({ name: "cipher2" })); + importResult.folderRelationships.push([0, 0]); + + await importService["setImportTarget"](importResult, "", mockImportTargetFolder); + expect(importResult.folderRelationships.length).toEqual(2); + expect(importResult.folderRelationships[0]).toEqual([1, 0]); + expect(importResult.folderRelationships[1]).toEqual([0, 1]); + }); }); }); + +function createCipher(options: Partial = {}) { + const cipher = new CipherView(); + + cipher.name; + cipher.type = options.type; + cipher.folderId = options.folderId; + cipher.collectionIds = options.collectionIds; + cipher.organizationId = options.organizationId; + + return cipher; +} diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index a6fd233dcf6c..62961a77c4c1 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -437,8 +437,10 @@ export class ImportService implements ImportServiceAbstraction { const noCollectionRelationShips: [number, number][] = []; importResult.ciphers.forEach((c, index) => { - if (!Array.isArray(c.collectionIds) || c.collectionIds.length == 0) { - c.collectionIds = [importTarget.id]; + if ( + !Array.isArray(importResult.collectionRelationships) || + !importResult.collectionRelationships.some(([cipherPos]) => cipherPos === index) + ) { noCollectionRelationShips.push([index, 0]); } }); From 5184dd956210e4a4b9b9de446b9e19a0df831f6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:31:31 -0500 Subject: [PATCH 03/51] [deps] AC: Update core-js to v3.36.1 (#8472) Co-authored-by: renovate[bot] <29139614+renovate[bot]@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 43f1987c88d3..c1a2f36237e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "bufferutil": "4.0.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.34.0", + "core-js": "3.36.1", "duo_web_sdk": "github:duosecurity/duo_web_sdk", "form-data": "4.0.0", "https-proxy-agent": "7.0.2", @@ -16583,9 +16583,9 @@ } }, "node_modules/core-js": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.34.0.tgz", - "integrity": "sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==", + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz", + "integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==", "hasInstallScript": true, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index d1276287c1c1..04e22107ad66 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "bufferutil": "4.0.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.34.0", + "core-js": "3.36.1", "duo_web_sdk": "github:duosecurity/duo_web_sdk", "form-data": "4.0.0", "https-proxy-agent": "7.0.2", From 20ba9fb4bed84b05801ea5f11c8d415a8a6c247c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:36:48 -0500 Subject: [PATCH 04/51] [deps] AC: Update html-webpack-plugin to v5.6.0 (#8474) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 17 +++++++++++++---- package.json | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1a2f36237e2..5b834a9a87e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -149,7 +149,7 @@ "gulp-zip": "6.0.0", "html-loader": "4.2.0", "html-webpack-injector": "1.1.4", - "html-webpack-plugin": "5.5.4", + "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.5", @@ -22361,9 +22361,9 @@ "dev": true }, "node_modules/html-webpack-plugin": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.4.tgz", - "integrity": "sha512-3wNSaVVxdxcu0jd4FpQFoICdqgxs4zIQQvj+2yQKFfBOnLETQ6X5CDWdeasuGlSsooFlMkEioWDTqBv1wvw5Iw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -22380,7 +22380,16 @@ "url": "https://opencollective.com/html-webpack-plugin" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/html-webpack-plugin/node_modules/commander": { diff --git a/package.json b/package.json index 04e22107ad66..7595b423a497 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "gulp-zip": "6.0.0", "html-loader": "4.2.0", "html-webpack-injector": "1.1.4", - "html-webpack-plugin": "5.5.4", + "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.5", From 0957b54d03ee9b21509c524cdec147f64eab2f84 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:38:17 -0500 Subject: [PATCH 05/51] [deps] AC: Update copy-webpack-plugin to v12 (#8478) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 213 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 186 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b834a9a87e6..c89e78a78f9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,7 +124,7 @@ "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", - "copy-webpack-plugin": "11.0.0", + "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.8.1", "electron": "28.2.8", @@ -614,6 +614,95 @@ "postcss": "^8.1.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -678,6 +767,25 @@ "node": ">=8.6.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -866,6 +974,18 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/@angular-devkit/build-angular/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/webpack": { "version": "5.88.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", @@ -7250,6 +7370,18 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -16516,20 +16648,20 @@ "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==" }, "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, "dependencies": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -16552,28 +16684,29 @@ } }, "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", - "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "node_modules/copy-webpack-plugin/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, "engines": { "node": ">=12" @@ -16582,6 +16715,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/core-js": { "version": "3.36.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz", @@ -33432,9 +33577,9 @@ } }, "node_modules/schema-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", - "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", @@ -33635,9 +33780,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -36891,6 +37036,18 @@ "tiny-inflate": "^1.0.0" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", diff --git a/package.json b/package.json index 7595b423a497..a7a9ee3f2b03 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", - "copy-webpack-plugin": "11.0.0", + "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.8.1", "electron": "28.2.8", From 899172722af5826e0a27efb18782a0b13ee7d95a Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:26:27 -0400 Subject: [PATCH 06/51] Auth/PM-5263 - TokenService State provider migration bug fix to avoid persisting tokens in local storage (#8413) * PM-5263 - Update token svc state provider migration to avoid persisting secrets that shouldn't exist in local storage to state provider local storage using new migration helper type. * PM-5263 - TokenSvc migration - tests TODO * write tests for migration * fix tests --------- Co-authored-by: Jake Fink --- ...igrate-token-svc-to-state-provider.spec.ts | 144 +++++++++++------- .../38-migrate-token-svc-to-state-provider.ts | 20 ++- 2 files changed, 109 insertions(+), 55 deletions(-) diff --git a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts index a5243c261a56..7dae6eeeb6d5 100644 --- a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts +++ b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts @@ -124,65 +124,107 @@ describe("TokenServiceStateProviderMigrator", () => { sut = new TokenServiceStateProviderMigrator(37, 38); }); - it("should remove state service data from all accounts that have it", async () => { - await sut.migrate(helper); + describe("Session storage", () => { + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); - expect(helper.set).toHaveBeenCalledWith("user1", { - tokens: { - otherStuff: "overStuff2", - }, - profile: { - email: "user1Email", - otherStuff: "overStuff3", - }, - keys: { - otherStuff: "overStuff4", - }, - otherStuff: "otherStuff5", + expect(helper.set).toHaveBeenCalledWith("user1", { + tokens: { + otherStuff: "overStuff2", + }, + profile: { + email: "user1Email", + otherStuff: "overStuff3", + }, + keys: { + otherStuff: "overStuff4", + }, + otherStuff: "otherStuff5", + }); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + expect(helper.set).not.toHaveBeenCalledWith("user3", any()); }); - expect(helper.set).toHaveBeenCalledTimes(2); - expect(helper.set).not.toHaveBeenCalledWith("user2", any()); - expect(helper.set).not.toHaveBeenCalledWith("user3", any()); - }); + it("should migrate data to state providers for defined accounts that have the data", async () => { + await sut.migrate(helper); - it("should migrate data to state providers for defined accounts that have the data", async () => { - await sut.migrate(helper); + // Two factor Token Migration + expect(helper.setToGlobal).toHaveBeenLastCalledWith( + EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, + { + user1Email: "twoFactorToken", + user2Email: "twoFactorToken", + }, + ); + expect(helper.setToGlobal).toHaveBeenCalledTimes(1); - // Two factor Token Migration - expect(helper.setToGlobal).toHaveBeenLastCalledWith( - EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, - { - user1Email: "twoFactorToken", - user2Email: "twoFactorToken", - }, - ); - expect(helper.setToGlobal).toHaveBeenCalledTimes(1); - - expect(helper.setToUser).toHaveBeenCalledWith("user1", ACCESS_TOKEN_DISK, "accessToken"); - expect(helper.setToUser).toHaveBeenCalledWith("user1", REFRESH_TOKEN_DISK, "refreshToken"); - expect(helper.setToUser).toHaveBeenCalledWith( - "user1", - API_KEY_CLIENT_ID_DISK, - "apiKeyClientId", - ); - expect(helper.setToUser).toHaveBeenCalledWith( - "user1", - API_KEY_CLIENT_SECRET_DISK, - "apiKeyClientSecret", - ); + expect(helper.setToUser).toHaveBeenCalledWith("user1", ACCESS_TOKEN_DISK, "accessToken"); + expect(helper.setToUser).toHaveBeenCalledWith("user1", REFRESH_TOKEN_DISK, "refreshToken"); + expect(helper.setToUser).toHaveBeenCalledWith( + "user1", + API_KEY_CLIENT_ID_DISK, + "apiKeyClientId", + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user1", + API_KEY_CLIENT_SECRET_DISK, + "apiKeyClientSecret", + ); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", ACCESS_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", REFRESH_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", API_KEY_CLIENT_ID_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", API_KEY_CLIENT_SECRET_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", ACCESS_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", REFRESH_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", API_KEY_CLIENT_ID_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith( + "user2", + API_KEY_CLIENT_SECRET_DISK, + any(), + ); - // Expect that we didn't migrate anything to user 3 + // Expect that we didn't migrate anything to user 3 - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", ACCESS_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", REFRESH_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", API_KEY_CLIENT_ID_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", API_KEY_CLIENT_SECRET_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", ACCESS_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", REFRESH_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", API_KEY_CLIENT_ID_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith( + "user3", + API_KEY_CLIENT_SECRET_DISK, + any(), + ); + }); + }); + describe("Local storage", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 37, "web-disk-local"); + }); + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + tokens: { + otherStuff: "overStuff2", + }, + profile: { + email: "user1Email", + otherStuff: "overStuff3", + }, + keys: { + otherStuff: "overStuff4", + }, + otherStuff: "otherStuff5", + }); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + expect(helper.set).not.toHaveBeenCalledWith("user3", any()); + }); + + it("should not migrate any data to local storage", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).not.toHaveBeenCalled(); + }); }); }); diff --git a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts index 17753d21879f..640e63cdc54a 100644 --- a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts @@ -84,7 +84,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { if (existingAccessToken != null) { // Only migrate data that exists - await helper.setToUser(userId, ACCESS_TOKEN_DISK, existingAccessToken); + if (helper.type !== "web-disk-local") { + // only migrate access token to session storage - never local. + await helper.setToUser(userId, ACCESS_TOKEN_DISK, existingAccessToken); + } delete account.tokens.accessToken; updatedAccount = true; } @@ -93,7 +96,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { const existingRefreshToken = account?.tokens?.refreshToken; if (existingRefreshToken != null) { - await helper.setToUser(userId, REFRESH_TOKEN_DISK, existingRefreshToken); + if (helper.type !== "web-disk-local") { + // only migrate refresh token to session storage - never local. + await helper.setToUser(userId, REFRESH_TOKEN_DISK, existingRefreshToken); + } delete account.tokens.refreshToken; updatedAccount = true; } @@ -102,7 +108,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { const existingApiKeyClientId = account?.profile?.apiKeyClientId; if (existingApiKeyClientId != null) { - await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, existingApiKeyClientId); + if (helper.type !== "web-disk-local") { + // only migrate client id to session storage - never local. + await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, existingApiKeyClientId); + } delete account.profile.apiKeyClientId; updatedAccount = true; } @@ -110,7 +119,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { // Migrate API key client secret const existingApiKeyClientSecret = account?.keys?.apiKeyClientSecret; if (existingApiKeyClientSecret != null) { - await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, existingApiKeyClientSecret); + if (helper.type !== "web-disk-local") { + // only migrate client secret to session storage - never local. + await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, existingApiKeyClientSecret); + } delete account.keys.apiKeyClientSecret; updatedAccount = true; } From 4873f649a9b13be021f73a6ad526c7ca6fee6f4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:27:45 -0500 Subject: [PATCH 07/51] [deps] AC: Update webpack-dev-server to v5 (#8482) * [deps] AC: Update webpack-dev-server to v5 * Update proxy object to be an array --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Addison Beck --- apps/web/webpack.config.js | 19 +- package-lock.json | 582 ++++++++++++++++++++++++++++++------- package.json | 2 +- 3 files changed, 492 insertions(+), 111 deletions(-) diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index f088aadb756a..815a8aff9e34 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -194,39 +194,44 @@ const devServer = }, }, // host: '192.168.1.9', - proxy: { - "/api": { + proxy: [ + { + context: ["/api"], target: envConfig.dev?.proxyApi, pathRewrite: { "^/api": "" }, secure: false, changeOrigin: true, }, - "/identity": { + { + context: ["/identity"], target: envConfig.dev?.proxyIdentity, pathRewrite: { "^/identity": "" }, secure: false, changeOrigin: true, }, - "/events": { + { + context: ["/events"], target: envConfig.dev?.proxyEvents, pathRewrite: { "^/events": "" }, secure: false, changeOrigin: true, }, - "/notifications": { + { + context: ["/notifications"], target: envConfig.dev?.proxyNotifications, pathRewrite: { "^/notifications": "" }, secure: false, changeOrigin: true, ws: true, }, - "/icons": { + { + context: ["/icons"], target: envConfig.dev?.proxyIcons, pathRewrite: { "^/icons": "" }, secure: false, changeOrigin: true, }, - }, + ], headers: (req) => { if (!req.originalUrl.includes("connector.html")) { return { diff --git a/package-lock.json b/package-lock.json index c89e78a78f9c..b9e050a22a8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,7 +183,7 @@ "wait-on": "7.2.0", "webpack": "5.89.0", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1", + "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0" }, "engines": { @@ -767,6 +767,26 @@ "node": ">=8.6.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/globby": { "version": "13.2.2", "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", @@ -834,6 +854,15 @@ "tslib": "^2.1.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -902,6 +931,21 @@ "webpack": "^5.0.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/sass": { "version": "1.64.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz", @@ -1033,6 +1077,162 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@angular-devkit/build-webpack": { "version": "0.1602.11", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.11.tgz", @@ -10977,9 +11177,9 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -11017,9 +11217,9 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -11125,9 +11325,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -11619,20 +11819,21 @@ } }, "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } @@ -11644,9 +11845,9 @@ "dev": true }, "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "dependencies": { "@types/node": "*" @@ -11714,6 +11915,15 @@ "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -14889,23 +15099,15 @@ } }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, - "node_modules/bonjour-service/node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -15303,6 +15505,21 @@ "semver": "^7.0.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -17239,6 +17456,22 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-browser-id": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", @@ -17255,6 +17488,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-browser/node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -17681,12 +17926,6 @@ "dev": true, "optional": true }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", @@ -23612,6 +23851,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -23672,6 +23944,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -25735,13 +26019,13 @@ } }, "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", "dev": true, "dependencies": { "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "shell-quote": "^1.8.1" } }, "node_modules/lazy-universal-dotenv": { @@ -33347,6 +33631,18 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -33617,11 +33913,12 @@ "dev": true }, "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -38080,54 +38377,54 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", "dev": true, "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "default-gateway": "^6.0.3", "express": "^4.17.3", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", + "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -38138,33 +38435,46 @@ } } }, - "node_modules/webpack-dev-server/node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "node_modules/webpack-dev-server/node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "dependencies": { - "@types/node": "*" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/webpack-dev-server/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { @@ -38176,48 +38486,114 @@ "node": ">= 10" } }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/webpack-dev-server/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "is-inside-container": "^1.0.0" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">=16" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/memfs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.8.0.tgz", + "integrity": "sha512-fcs7trFxZlOMadmTw5nyfOwS3il9pr3y+6xzLfXNwmuR/D0i4wz6rJURxArAbcJDGalbpbMvQ/IFI0NojRZgRg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.1.1.tgz", + "integrity": "sha512-NmRVq4AvRQs66dFWyDR4GsFDJggtSi2Yn38MXLk0nffgF9n/AIP4TFBg2TQKYaRAN4sHuKOTiz9BnNCENDLEVA==", "dev": true, "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index a7a9ee3f2b03..e04d7139dc16 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "wait-on": "7.2.0", "webpack": "5.89.0", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1", + "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0" }, "dependencies": { From da14d01062d2987e4599aa4c877f74ab7e7ddb89 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Mon, 25 Mar 2024 16:50:33 -0400 Subject: [PATCH 08/51] [PM-6927] update date for onboarding component to release (#8487) --- .../vault-onboarding/vault-onboarding.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 22f56a85a9d6..16f68d6111b3 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -43,7 +43,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { isIndividualPolicyVault: boolean; private destroy$ = new Subject(); isNewAccount: boolean; - private readonly onboardingReleaseDate = new Date("2024-01-01"); + private readonly onboardingReleaseDate = new Date("2024-04-02"); showOnboardingAccess$: Observable; protected currentTasks: VaultOnboardingTasks; From d000f081da274a1928589c7ddae2a858d98b0efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 26 Mar 2024 07:59:45 -0400 Subject: [PATCH 09/51] [PM-6556] reintroduce policy reduction for multi-org accounts (#8409) --- .../generator-strategy.abstraction.ts | 9 ++- .../default-generator.service.spec.ts | 36 ++++++------ .../generator/default-generator.service.ts | 14 ++--- .../passphrase-generator-policy.spec.ts | 51 +++++++++++++++++ .../passphrase/passphrase-generator-policy.ts | 26 +++++++++ .../passphrase-generator-strategy.spec.ts | 34 ++++++------ .../passphrase-generator-strategy.ts | 31 ++++------- .../password-generator-policy.spec.ts | 55 +++++++++++++++++++ .../password/password-generator-policy.ts | 27 +++++++++ .../password-generator-strategy.spec.ts | 34 ++++++------ .../password/password-generator-strategy.ts | 33 ++++------- .../reduce-collection.operator.spec.ts | 33 +++++++++++ .../generator/reduce-collection.operator.ts | 20 +++++++ .../catchall-generator-strategy.spec.ts | 50 +++++++---------- .../username/catchall-generator-strategy.ts | 18 ++---- .../eff-username-generator-strategy.spec.ts | 50 +++++++---------- .../eff-username-generator-strategy.ts | 18 ++---- .../forwarder-generator-strategy.spec.ts | 25 +++++++-- .../username/forwarder-generator-strategy.ts | 9 +-- .../subaddress-generator-strategy.spec.ts | 50 +++++++---------- .../username/subaddress-generator-strategy.ts | 18 ++---- 21 files changed, 394 insertions(+), 247 deletions(-) create mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts create mode 100644 libs/common/src/tools/generator/password/password-generator-policy.spec.ts create mode 100644 libs/common/src/tools/generator/reduce-collection.operator.spec.ts create mode 100644 libs/common/src/tools/generator/reduce-collection.operator.ts diff --git a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts index f11c1d730097..eda02f7cdcbe 100644 --- a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts @@ -1,3 +1,5 @@ +import { Observable } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 @@ -21,13 +23,16 @@ export abstract class GeneratorStrategy { /** Length of time in milliseconds to cache the evaluator */ cache_ms: number; - /** Creates an evaluator from a generator policy. + /** Operator function that converts a policy collection observable to a single + * policy evaluator observable. * @param policy The policy being evaluated. * @returns the policy evaluator. If `policy` is is `null` or `undefined`, * then the evaluator defaults to the application's limits. * @throws when the policy's type does not match the generator's policy type. */ - evaluator: (policy: AdminPolicy) => PolicyEvaluator; + toEvaluator: () => ( + source: Observable, + ) => Observable>; /** Generates credentials from the given options. * @param options The options used to generate the credentials. diff --git a/libs/common/src/tools/generator/default-generator.service.spec.ts b/libs/common/src/tools/generator/default-generator.service.spec.ts index 84b8ff453035..53a46c4963ed 100644 --- a/libs/common/src/tools/generator/default-generator.service.spec.ts +++ b/libs/common/src/tools/generator/default-generator.service.spec.ts @@ -4,7 +4,7 @@ */ import { mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, pipe } from "rxjs"; import { FakeSingleUserState, awaitAsync } from "../../../spec"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; @@ -20,12 +20,12 @@ import { PasswordGenerationOptions } from "./password"; import { DefaultGeneratorService } from "."; -function mockPolicyService(config?: { state?: BehaviorSubject }) { +function mockPolicyService(config?: { state?: BehaviorSubject }) { const service = mock(); // FIXME: swap out the mock return value when `getAll$` becomes available - const stateValue = config?.state ?? new BehaviorSubject(null); - service.get$.mockReturnValue(stateValue); + const stateValue = config?.state ?? new BehaviorSubject([null]); + service.getAll$.mockReturnValue(stateValue); // const stateValue = config?.state ?? new BehaviorSubject(null); // service.getAll$.mockReturnValue(stateValue); @@ -46,7 +46,9 @@ function mockGeneratorStrategy(config?: { // the value from `config`. durableState: jest.fn(() => durableState), policy: config?.policy ?? PolicyType.DisableSend, - evaluator: jest.fn(() => config?.evaluator ?? mock>()), + toEvaluator: jest.fn(() => + pipe(map(() => config?.evaluator ?? mock>())), + ), }); return strategy; @@ -94,9 +96,7 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); - // FIXME: swap out the expect when `getAll$` becomes available - expect(policy.get$).toHaveBeenCalledWith(PolicyType.PasswordGenerator); - //expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); + expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); }); it("should map the policy using the generation strategy", async () => { @@ -112,21 +112,22 @@ describe("Password generator service", () => { it("should update the evaluator when the password generator policy changes", async () => { // set up dependencies - const state = new BehaviorSubject(null); + const state = new BehaviorSubject([null]); const policy = mockPolicyService({ state }); const strategy = mockGeneratorStrategy(); const service = new DefaultGeneratorService(strategy, policy); - // model responses for the observable update + // model responses for the observable update. The map is called multiple times, + // and the array shift ensures reference equality is maintained. const firstEvaluator = mock>(); - strategy.evaluator.mockReturnValueOnce(firstEvaluator); const secondEvaluator = mock>(); - strategy.evaluator.mockReturnValueOnce(secondEvaluator); + const evaluators = [firstEvaluator, secondEvaluator]; + strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift()))); // act const evaluator$ = service.evaluator$(SomeUser); const firstResult = await firstValueFrom(evaluator$); - state.next(null); + state.next([null]); const secondResult = await firstValueFrom(evaluator$); // assert @@ -142,9 +143,7 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); await firstValueFrom(service.evaluator$(SomeUser)); - // FIXME: swap out the expect when `getAll$` becomes available - expect(policy.get$).toHaveBeenCalledTimes(1); - //expect(policy.getAll$).toHaveBeenCalledTimes(1); + expect(policy.getAll$).toHaveBeenCalledTimes(1); }); it("should cache the password generator policy for each user", async () => { @@ -155,9 +154,8 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); await firstValueFrom(service.evaluator$(AnotherUser)); - // FIXME: enable this test when `getAll$` becomes available - // expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser); - // expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser); + expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser); + expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser); }); }); diff --git a/libs/common/src/tools/generator/default-generator.service.ts b/libs/common/src/tools/generator/default-generator.service.ts index 9c884ccefdce..34aacee695c6 100644 --- a/libs/common/src/tools/generator/default-generator.service.ts +++ b/libs/common/src/tools/generator/default-generator.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, share, timer, ReplaySubject, Observable } from "rxjs"; +import { firstValueFrom, share, timer, ReplaySubject, Observable } from "rxjs"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 @@ -44,14 +44,12 @@ export class DefaultGeneratorService implements GeneratorServic } private createEvaluator(userId: UserId) { - // FIXME: when it becomes possible to get a user-specific policy observable - // (`getAll$`) update this code to call it instead of `get$`. - const policies$ = this.policy.get$(this.strategy.policy); + const evaluator$ = this.policy.getAll$(this.strategy.policy, userId).pipe( + // create the evaluator from the policies + this.strategy.toEvaluator(), - // cache evaluator in a replay subject to amortize creation cost - // and reduce GC pressure. - const evaluator$ = policies$.pipe( - map((policy) => this.strategy.evaluator(policy)), + // cache evaluator in a replay subject to amortize creation cost + // and reduce GC pressure. share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(this.strategy.cache_ms), diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts new file mode 100644 index 000000000000..991b2ae30246 --- /dev/null +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts @@ -0,0 +1,51 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { PolicyId } from "../../../types/guid"; + +import { DisabledPassphraseGeneratorPolicy, leastPrivilege } from "./passphrase-generator-policy"; + +function createPolicy( + data: any, + type: PolicyType = PolicyType.PasswordGenerator, + enabled: boolean = true, +) { + return new Policy({ + id: "id" as PolicyId, + organizationId: "organizationId", + data, + enabled, + type, + }); +} + +describe("leastPrivilege", () => { + it("should return the accumulator when the policy type does not apply", () => { + const policy = createPolicy({}, PolicyType.RequireSso); + + const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPassphraseGeneratorPolicy); + }); + + it("should return the accumulator when the policy is not enabled", () => { + const policy = createPolicy({}, PolicyType.PasswordGenerator, false); + + const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPassphraseGeneratorPolicy); + }); + + it.each([ + ["minNumberWords", 10], + ["capitalize", true], + ["includeNumber", true], + ])("should take the %p from the policy", (input, value) => { + const policy = createPolicy({ [input]: value }); + + const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); + + expect(result).toEqual({ ...DisabledPassphraseGeneratorPolicy, [input]: value }); + }); +}); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts index ca54184d166b..db616f16c05a 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts @@ -1,3 +1,8 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; + /** Policy options enforced during passphrase generation. */ export type PassphraseGeneratorPolicy = { minNumberWords: number; @@ -11,3 +16,24 @@ export const DisabledPassphraseGeneratorPolicy: PassphraseGeneratorPolicy = Obje capitalize: false, includeNumber: false, }); + +/** Reduces a policy into an accumulator by accepting the most restrictive + * values from each policy. + * @param acc the accumulator + * @param policy the policy to reduce + * @returns the most restrictive values between the policy and accumulator. + */ +export function leastPrivilege( + acc: PassphraseGeneratorPolicy, + policy: Policy, +): PassphraseGeneratorPolicy { + if (policy.type !== PolicyType.PasswordGenerator) { + return acc; + } + + return { + minNumberWords: Math.max(acc.minNumberWords, policy.data.minNumberWords ?? acc.minNumberWords), + capitalize: policy.data.capitalize || acc.capitalize, + includeNumber: policy.data.includeNumber || acc.includeNumber, + }; +} diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts index 031ea05f0147..b7f09bd717d5 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts @@ -4,6 +4,7 @@ */ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -21,17 +22,8 @@ import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from const SomeUser = "some user" as UserId; describe("Password generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new PassphraseGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.DisableSend, - }); - - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); - - it("should map to the policy evaluator", () => { + describe("toEvaluator()", () => { + it("should map to the policy evaluator", async () => { const strategy = new PassphraseGeneratorStrategy(null, null); const policy = mock({ type: PolicyType.PasswordGenerator, @@ -42,7 +34,8 @@ describe("Password generation strategy", () => { }, }); - const evaluator = strategy.evaluator(policy); + const evaluator$ = of([policy]).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); expect(evaluator.policy).toMatchObject({ @@ -52,13 +45,18 @@ describe("Password generation strategy", () => { }); }); - it("should map `null` to a default policy evaluator", () => { - const strategy = new PassphraseGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); + it.each([[[]], [null], [undefined]])( + "should map `%p` to a disabled password policy evaluator", + async (policies) => { + const strategy = new PassphraseGeneratorStrategy(null, null); - expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy); - }); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); + + expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); + expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy); + }, + ); }); describe("durableState", () => { diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts index d39f54b57657..f193b2b32661 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts @@ -1,18 +1,19 @@ +import { map, pipe } from "rxjs"; + import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { PASSPHRASE_SETTINGS } from "../key-definitions"; import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction"; +import { reduceCollection } from "../reduce-collection.operator"; import { PassphraseGenerationOptions } from "./passphrase-generation-options"; import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; import { DisabledPassphraseGeneratorPolicy, PassphraseGeneratorPolicy, + leastPrivilege, } from "./passphrase-generator-policy"; const ONE_MINUTE = 60 * 1000; @@ -23,6 +24,7 @@ export class PassphraseGeneratorStrategy { /** instantiates the password generator strategy. * @param legacy generates the passphrase + * @param stateProvider provides durable state */ constructor( private legacy: PasswordGenerationServiceAbstraction, @@ -39,26 +41,17 @@ export class PassphraseGeneratorStrategy return PolicyType.PasswordGenerator; } + /** {@link GeneratorStrategy.cache_ms} */ get cache_ms() { return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator { - if (!policy) { - return new PassphraseGeneratorOptionsEvaluator(DisabledPassphraseGeneratorPolicy); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new PassphraseGeneratorOptionsEvaluator({ - minNumberWords: policy.data.minNumberWords, - capitalize: policy.data.capitalize, - includeNumber: policy.data.includeNumber, - }); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe( + reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy), + map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)), + ); } /** {@link GeneratorStrategy.generate} */ diff --git a/libs/common/src/tools/generator/password/password-generator-policy.spec.ts b/libs/common/src/tools/generator/password/password-generator-policy.spec.ts new file mode 100644 index 000000000000..206d88741b04 --- /dev/null +++ b/libs/common/src/tools/generator/password/password-generator-policy.spec.ts @@ -0,0 +1,55 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { PolicyId } from "../../../types/guid"; + +import { DisabledPasswordGeneratorPolicy, leastPrivilege } from "./password-generator-policy"; + +function createPolicy( + data: any, + type: PolicyType = PolicyType.PasswordGenerator, + enabled: boolean = true, +) { + return new Policy({ + id: "id" as PolicyId, + organizationId: "organizationId", + data, + enabled, + type, + }); +} + +describe("leastPrivilege", () => { + it("should return the accumulator when the policy type does not apply", () => { + const policy = createPolicy({}, PolicyType.RequireSso); + + const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPasswordGeneratorPolicy); + }); + + it("should return the accumulator when the policy is not enabled", () => { + const policy = createPolicy({}, PolicyType.PasswordGenerator, false); + + const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPasswordGeneratorPolicy); + }); + + it.each([ + ["minLength", 10, "minLength"], + ["useUpper", true, "useUppercase"], + ["useLower", true, "useLowercase"], + ["useNumbers", true, "useNumbers"], + ["minNumbers", 10, "numberCount"], + ["useSpecial", true, "useSpecial"], + ["minSpecial", 10, "specialCount"], + ])("should take the %p from the policy", (input, value, expected) => { + const policy = createPolicy({ [input]: value }); + + const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); + + expect(result).toEqual({ ...DisabledPasswordGeneratorPolicy, [expected]: value }); + }); +}); diff --git a/libs/common/src/tools/generator/password/password-generator-policy.ts b/libs/common/src/tools/generator/password/password-generator-policy.ts index c28631e9dec1..7de6b49788d6 100644 --- a/libs/common/src/tools/generator/password/password-generator-policy.ts +++ b/libs/common/src/tools/generator/password/password-generator-policy.ts @@ -1,3 +1,8 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; + /** Policy options enforced during password generation. */ export type PasswordGeneratorPolicy = { /** The minimum length of generated passwords. @@ -48,3 +53,25 @@ export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.f useSpecial: false, specialCount: 0, }); + +/** Reduces a policy into an accumulator by accepting the most restrictive + * values from each policy. + * @param acc the accumulator + * @param policy the policy to reduce + * @returns the most restrictive values between the policy and accumulator. + */ +export function leastPrivilege(acc: PasswordGeneratorPolicy, policy: Policy) { + if (policy.type !== PolicyType.PasswordGenerator || !policy.enabled) { + return acc; + } + + return { + minLength: Math.max(acc.minLength, policy.data.minLength ?? acc.minLength), + useUppercase: policy.data.useUpper || acc.useUppercase, + useLowercase: policy.data.useLower || acc.useLowercase, + useNumbers: policy.data.useNumbers || acc.useNumbers, + numberCount: Math.max(acc.numberCount, policy.data.minNumbers ?? acc.numberCount), + useSpecial: policy.data.useSpecial || acc.useSpecial, + specialCount: Math.max(acc.specialCount, policy.data.minSpecial ?? acc.specialCount), + }; +} diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts index 6c213f8c543a..9bfa5b5f3529 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts @@ -4,6 +4,7 @@ */ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -24,17 +25,8 @@ import { const SomeUser = "some user" as UserId; describe("Password generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new PasswordGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.DisableSend, - }); - - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); - - it("should map to the policy evaluator", () => { + describe("toEvaluator()", () => { + it("should map to a password policy evaluator", async () => { const strategy = new PasswordGeneratorStrategy(null, null); const policy = mock({ type: PolicyType.PasswordGenerator, @@ -49,7 +41,8 @@ describe("Password generation strategy", () => { }, }); - const evaluator = strategy.evaluator(policy); + const evaluator$ = of([policy]).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); expect(evaluator.policy).toMatchObject({ @@ -63,13 +56,18 @@ describe("Password generation strategy", () => { }); }); - it("should map `null` to a default policy evaluator", () => { - const strategy = new PasswordGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); + it.each([[[]], [null], [undefined]])( + "should map `%p` to a disabled password policy evaluator", + async (policies) => { + const strategy = new PasswordGeneratorStrategy(null, null); - expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy); - }); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); + + expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); + expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy); + }, + ); }); describe("durableState", () => { diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.ts b/libs/common/src/tools/generator/password/password-generator-strategy.ts index 223470c58691..f8d618128b19 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.ts @@ -1,11 +1,11 @@ +import { map, pipe } from "rxjs"; + import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { PASSWORD_SETTINGS } from "../key-definitions"; +import { reduceCollection } from "../reduce-collection.operator"; import { PasswordGenerationOptions } from "./password-generation-options"; import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; @@ -13,6 +13,7 @@ import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options- import { DisabledPasswordGeneratorPolicy, PasswordGeneratorPolicy, + leastPrivilege, } from "./password-generator-policy"; const ONE_MINUTE = 60 * 1000; @@ -43,26 +44,12 @@ export class PasswordGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator { - if (!policy) { - return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new PasswordGeneratorOptionsEvaluator({ - minLength: policy.data.minLength, - useUppercase: policy.data.useUpper, - useLowercase: policy.data.useLower, - useNumbers: policy.data.useNumbers, - numberCount: policy.data.minNumbers, - useSpecial: policy.data.useSpecial, - specialCount: policy.data.minSpecial, - }); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe( + reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy), + map((policy) => new PasswordGeneratorOptionsEvaluator(policy)), + ); } /** {@link GeneratorStrategy.generate} */ diff --git a/libs/common/src/tools/generator/reduce-collection.operator.spec.ts b/libs/common/src/tools/generator/reduce-collection.operator.spec.ts new file mode 100644 index 000000000000..49648dfdf006 --- /dev/null +++ b/libs/common/src/tools/generator/reduce-collection.operator.spec.ts @@ -0,0 +1,33 @@ +/** + * include structuredClone in test environment. + * @jest-environment ../../../../shared/test.environment.ts + */ + +import { of, firstValueFrom } from "rxjs"; + +import { reduceCollection } from "./reduce-collection.operator"; + +describe("reduceCollection", () => { + it.each([[null], [undefined], [[]]])( + "should return the default value when the collection is %p", + async (value: number[]) => { + const reduce = (acc: number, value: number) => acc + value; + const source$ = of(value); + + const result$ = source$.pipe(reduceCollection(reduce, 100)); + const result = await firstValueFrom(result$); + + expect(result).toEqual(100); + }, + ); + + it("should reduce the collection to a single value", async () => { + const reduce = (acc: number, value: number) => acc + value; + const source$ = of([1, 2, 3]); + + const result$ = source$.pipe(reduceCollection(reduce, 0)); + const result = await firstValueFrom(result$); + + expect(result).toEqual(6); + }); +}); diff --git a/libs/common/src/tools/generator/reduce-collection.operator.ts b/libs/common/src/tools/generator/reduce-collection.operator.ts new file mode 100644 index 000000000000..224595eeba23 --- /dev/null +++ b/libs/common/src/tools/generator/reduce-collection.operator.ts @@ -0,0 +1,20 @@ +import { map, OperatorFunction } from "rxjs"; + +/** + * An observable operator that reduces an emitted collection to a single object, + * returning a default if all items are ignored. + * @param reduce The reduce function to apply to the filtered collection. The + * first argument is the accumulator, and the second is the current item. The + * return value is the new accumulator. + * @param defaultValue The default value to return if the collection is empty. The + * default value is also the initial value of the accumulator. + */ +export function reduceCollection( + reduce: (acc: Accumulator, value: Item) => Accumulator, + defaultValue: Accumulator, +): OperatorFunction { + return map((values: Item[]) => { + const reduced = (values ?? []).reduce(reduce, structuredClone(defaultValue)); + return reduced; + }); +} diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts index dafb55febab4..339e4b27203c 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts @@ -1,4 +1,5 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -12,39 +13,26 @@ import { CATCHALL_SETTINGS } from "../key-definitions"; import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("Email subaddress list generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new CatchallGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.DisableSend, - }); - - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); - - it("should map to the policy evaluator", () => { - const strategy = new CatchallGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, - }); - - const evaluator = strategy.evaluator(policy); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - expect(evaluator.policy).toMatchObject({}); - }); - - it("should map `null` to a default policy evaluator", () => { - const strategy = new CatchallGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new CatchallGeneratorStrategy(null, null); + + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); + + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); describe("durableState", () => { diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts index aadca78b3b4a..6b36ebd50b54 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts @@ -1,5 +1,6 @@ +import { map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; @@ -41,18 +42,9 @@ export class CatchallGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator())); } /** {@link GeneratorStrategy.generate} */ diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts index 0fb5bf573c00..821b4bb7dc82 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts @@ -1,4 +1,5 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -12,39 +13,26 @@ import { EFF_USERNAME_SETTINGS } from "../key-definitions"; import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("EFF long word list generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.DisableSend, - }); - - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); - - it("should map to the policy evaluator", () => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, - }); - - const evaluator = strategy.evaluator(policy); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - expect(evaluator.policy).toMatchObject({}); - }); - - it("should map `null` to a default policy evaluator", () => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new EffUsernameGeneratorStrategy(null, null); + + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); + + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); describe("durableState", () => { diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts index e0179895ae3a..133b4e77776c 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts @@ -1,5 +1,6 @@ +import { map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; @@ -41,18 +42,9 @@ export class EffUsernameGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator())); } /** {@link GeneratorStrategy.generate} */ diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts index 96a7bca2b1d9..30dd62048439 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts @@ -1,6 +1,11 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { StateProvider } from "../../../platform/state"; @@ -29,6 +34,12 @@ class TestForwarder extends ForwarderGeneratorStrategy { const SomeUser = "some user" as UserId; const AnotherUser = "another user" as UserId; +const SomePolicy = mock({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("ForwarderGeneratorStrategy", () => { const encryptService = mock(); @@ -63,11 +74,17 @@ describe("ForwarderGeneratorStrategy", () => { }); }); - it("evaluator returns the default policy evaluator", () => { - const strategy = new TestForwarder(null, null, null); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new TestForwarder(encryptService, keyService, stateProvider); - const result = strategy.evaluator(null); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); - expect(result).toBeInstanceOf(DefaultPolicyEvaluator); + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); }); diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts index b0717695e05c..8b78f22634e1 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -1,5 +1,6 @@ +import { map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { KeyDefinition, SingleUserState, StateProvider } from "../../../platform/state"; @@ -81,8 +82,8 @@ export abstract class ForwarderGeneratorStrategy< /** Determine where forwarder configuration is stored */ protected abstract readonly key: KeyDefinition; - /** {@link GeneratorStrategy.evaluator} */ - evaluator = (_policy: Policy) => { - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator = () => { + return pipe(map((_) => new DefaultPolicyEvaluator())); }; } diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts index 105edd6b4dfa..59a2b56172af 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts @@ -1,4 +1,5 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -12,39 +13,26 @@ import { SUBADDRESS_SETTINGS } from "../key-definitions"; import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("Email subaddress list generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new SubaddressGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.DisableSend, - }); - - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); - - it("should map to the policy evaluator", () => { - const strategy = new SubaddressGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, - }); - - const evaluator = strategy.evaluator(policy); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - expect(evaluator.policy).toMatchObject({}); - }); - - it("should map `null` to a default policy evaluator", () => { - const strategy = new SubaddressGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new SubaddressGeneratorStrategy(null, null); + + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); + + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); describe("durableState", () => { diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts index 1aba473476d7..1ae0cb914275 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts @@ -1,5 +1,6 @@ +import { map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; @@ -41,18 +42,9 @@ export class SubaddressGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator())); } /** {@link GeneratorStrategy.generate} */ From a46767dee2e9c794edc9872693eacd99aca6b335 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Tue, 26 Mar 2024 09:56:20 -0400 Subject: [PATCH 10/51] add auth status to auth service (#8377) * add auth status to auth service * fix auth service factory --- .../service-factories/auth-service.factory.ts | 4 ++ .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../src/auth/abstractions/auth.service.ts | 9 ++- .../src/auth/services/auth.service.spec.ts | 61 +++++++++++++++++++ libs/common/src/auth/services/auth.service.ts | 14 ++++- 7 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 libs/common/src/auth/services/auth.service.spec.ts diff --git a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts index fa52ca6231c3..bc4e621bc6ef 100644 --- a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts @@ -23,9 +23,12 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory"; + type AuthServiceFactoryOptions = FactoryOptions; export type AuthServiceInitOptions = AuthServiceFactoryOptions & + AccountServiceInitOptions & MessagingServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & @@ -41,6 +44,7 @@ export function authServiceFactory( opts, async () => new AuthService( + await accountServiceFactory(cache, opts), await messagingServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3e84b7544b48..51417c16fb93 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -568,6 +568,7 @@ export default class MainBackground { ); this.authService = new AuthService( + this.accountService, backgroundMessagingService, this.cryptoService, this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7af40b1ebd9a..5c6423708aa9 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -494,6 +494,7 @@ export class Main { ); this.authService = new AuthService( + this.accountService, this.messagingService, this.cryptoService, this.apiService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index beda7cbf4f5f..57a3fe63ab52 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -345,6 +345,7 @@ const typesafeProviders: Array = [ provide: AuthServiceAbstraction, useClass: AuthService, deps: [ + AccountServiceAbstraction, MessagingServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, diff --git a/libs/common/src/auth/abstractions/auth.service.ts b/libs/common/src/auth/abstractions/auth.service.ts index dc51e2fdb0ff..9e4fd3cd0be5 100644 --- a/libs/common/src/auth/abstractions/auth.service.ts +++ b/libs/common/src/auth/abstractions/auth.service.ts @@ -1,6 +1,11 @@ +import { Observable } from "rxjs"; + import { AuthenticationStatus } from "../enums/authentication-status"; export abstract class AuthService { - getAuthStatus: (userId?: string) => Promise; - logOut: (callback: () => void) => void; + /** Authentication status for the active user */ + abstract activeAccountStatus$: Observable; + /** @deprecated use {@link activeAccountStatus$} instead */ + abstract getAuthStatus: (userId?: string) => Promise; + abstract logOut: (callback: () => void) => void; } diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts new file mode 100644 index 000000000000..dd4daf8cfa83 --- /dev/null +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -0,0 +1,61 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { ApiService } from "../../abstractions/api.service"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; +import { UserId } from "../../types/guid"; +import { AuthenticationStatus } from "../enums/authentication-status"; + +import { AuthService } from "./auth.service"; + +describe("AuthService", () => { + let sut: AuthService; + + let accountService: FakeAccountService; + let messagingService: MockProxy; + let cryptoService: MockProxy; + let apiService: MockProxy; + let stateService: MockProxy; + + const userId = Utils.newGuid() as UserId; + + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + messagingService = mock(); + cryptoService = mock(); + apiService = mock(); + stateService = mock(); + + sut = new AuthService( + accountService, + messagingService, + cryptoService, + apiService, + stateService, + ); + }); + + describe("activeAccountStatus$", () => { + test.each([ + AuthenticationStatus.LoggedOut, + AuthenticationStatus.Locked, + AuthenticationStatus.Unlocked, + ])( + `should emit %p when activeAccount$ emits an account with %p auth status`, + async (status) => { + accountService.activeAccountSubject.next({ + id: userId, + email: "email", + name: "name", + status, + }); + + expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(status); + }, + ); + }); +}); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 14d49956a430..ae5dd30a3645 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,18 +1,30 @@ +import { Observable, distinctUntilChanged, map, shareReplay } from "rxjs"; + import { ApiService } from "../../abstractions/api.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { StateService } from "../../platform/abstractions/state.service"; import { KeySuffixOptions } from "../../platform/enums"; +import { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; import { AuthenticationStatus } from "../enums/authentication-status"; export class AuthService implements AuthServiceAbstraction { + activeAccountStatus$: Observable; + constructor( + protected accountService: AccountService, protected messagingService: MessagingService, protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, - ) {} + ) { + this.activeAccountStatus$ = this.accountService.activeAccount$.pipe( + map((account) => account.status), + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: false }), + ); + } async getAuthStatus(userId?: string): Promise { // If we don't have an access token or userId, we're logged out From 7f14ee4994903c78fdc46cd0d551f0518f44597e Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Tue, 26 Mar 2024 12:00:30 -0400 Subject: [PATCH 11/51] add back call to verify by PIN (#8495) --- .../services/user-verification/user-verification.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 03e267d9db58..0b4cd9609931 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -140,7 +140,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti case VerificationType.MasterPassword: return this.verifyUserByMasterPassword(verification); case VerificationType.PIN: - break; + return this.verifyUserByPIN(verification); case VerificationType.Biometrics: return this.verifyUserByBiometrics(); default: { From f7014a973c541662dfce952915f19d6b9ff357e1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:06:33 -0500 Subject: [PATCH 12/51] [PM-7071] Fallback to Emitting `null` When No Active User (#8486) * Fallback to Emitting `null` When No Active User * Fix Tests * Update Test Names to Follow Convention Co-authored-by: Andreas Coroiu * Fix CLI Build --------- Co-authored-by: Andreas Coroiu --- .../browser/src/background/main.background.ts | 2 +- ...g-account-profile-state-service.factory.ts | 7 +- apps/cli/src/bw.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- ...ling-account-profile-state.service.spec.ts | 129 ++++++++---------- .../billing-account-profile-state.service.ts | 28 ++-- 6 files changed, 78 insertions(+), 92 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 51417c16fb93..452624d77e12 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -576,7 +576,7 @@ export default class MainBackground { ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.activeUserStateProvider, + this.stateProvider, ); this.loginStrategyService = new LoginStrategyService( diff --git a/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts index 80482eacb673..378707d6be30 100644 --- a/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts @@ -1,9 +1,8 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; -import { activeUserStateProviderFactory } from "./active-user-state-provider.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; -import { StateProviderInitOptions } from "./state-provider.factory"; +import { StateProviderInitOptions, stateProviderFactory } from "./state-provider.factory"; type BillingAccountProfileStateServiceFactoryOptions = FactoryOptions; @@ -21,8 +20,6 @@ export function billingAccountProfileStateServiceFactory( "billingAccountProfileStateService", opts, async () => - new DefaultBillingAccountProfileStateService( - await activeUserStateProviderFactory(cache, opts), - ), + new DefaultBillingAccountProfileStateService(await stateProviderFactory(cache, opts)), ); } diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 5c6423708aa9..360ac6ffc480 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -467,7 +467,7 @@ export class Main { ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.activeUserStateProvider, + this.stateProvider, ); this.loginStrategyService = new LoginStrategyService( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 57a3fe63ab52..cab71631da85 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1067,7 +1067,7 @@ const typesafeProviders: Array = [ safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, - deps: [ActiveUserStateProvider], + deps: [StateProvider], }), safeProvider({ provide: OrganizationManagementPreferencesService, diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts index 4a2a94e9c602..7f0f218a2398 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts @@ -2,10 +2,10 @@ import { firstValueFrom } from "rxjs"; import { FakeAccountService, - FakeActiveUserStateProvider, mockAccountServiceWith, FakeActiveUserState, - trackEmissions, + FakeStateProvider, + FakeSingleUserState, } from "../../../../spec"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service"; @@ -16,20 +16,26 @@ import { } from "./billing-account-profile-state.service"; describe("BillingAccountProfileStateService", () => { - let activeUserStateProvider: FakeActiveUserStateProvider; + let stateProvider: FakeStateProvider; let sut: DefaultBillingAccountProfileStateService; let billingAccountProfileState: FakeActiveUserState; + let userBillingAccountProfileState: FakeSingleUserState; let accountService: FakeAccountService; const userId = "fakeUserId" as UserId; beforeEach(() => { accountService = mockAccountServiceWith(userId); - activeUserStateProvider = new FakeActiveUserStateProvider(accountService); + stateProvider = new FakeStateProvider(accountService); - sut = new DefaultBillingAccountProfileStateService(activeUserStateProvider); + sut = new DefaultBillingAccountProfileStateService(stateProvider); - billingAccountProfileState = activeUserStateProvider.getFake( + billingAccountProfileState = stateProvider.activeUser.getFake( + BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, + ); + + userBillingAccountProfileState = stateProvider.singleUser.getFake( + userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, ); }); @@ -38,9 +44,9 @@ describe("BillingAccountProfileStateService", () => { return jest.resetAllMocks(); }); - describe("accountHasPremiumFromAnyOrganization$", () => { - it("should emit changes in hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ + describe("hasPremiumFromAnyOrganization$", () => { + it("returns true when they have premium from an organization", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: false, hasPremiumFromAnyOrganization: true, }); @@ -48,20 +54,25 @@ describe("BillingAccountProfileStateService", () => { expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); }); - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumFromAnyOrganization$); - const startingEmissionCount = emissions.length; + it("return false when they do not have premium from an organization", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); + }); - await sut.setHasPremium(true, true); + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); }); }); describe("hasPremiumPersonally$", () => { - it("should emit changes in hasPremiumPersonally", async () => { - billingAccountProfileState.nextState({ + it("returns true when the user has premium personally", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false, }); @@ -69,20 +80,25 @@ describe("BillingAccountProfileStateService", () => { expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); }); - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumPersonally$); - const startingEmissionCount = emissions.length; + it("returns false when the user does not have premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); + }); - await sut.setHasPremium(true, true); + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); }); }); - describe("canAccessPremium$", () => { - it("should emit changes in hasPremiumPersonally", async () => { - billingAccountProfileState.nextState({ + describe("hasPremiumFromAnySource$", () => { + it("returns true when the user has premium personally", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false, }); @@ -90,8 +106,8 @@ describe("BillingAccountProfileStateService", () => { expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); - it("should emit changes in hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ + it("returns true when the user has premium from an organization", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: false, hasPremiumFromAnyOrganization: true, }); @@ -99,8 +115,8 @@ describe("BillingAccountProfileStateService", () => { expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); - it("should emit changes in both hasPremiumPersonally and hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ + it("returns true when they have premium personally AND from an organization", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: true, hasPremiumFromAnyOrganization: true, }); @@ -108,58 +124,21 @@ describe("BillingAccountProfileStateService", () => { expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumFromAnySource$); - const startingEmissionCount = emissions.length; - - await sut.setHasPremium(true, true); + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false); }); }); describe("setHasPremium", () => { - it("should have `hasPremiumPersonally$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => { + it("should update the active users state when called", async () => { await sut.setHasPremium(true, false); - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); - }); - - it("should have `hasPremiumFromAnyOrganization$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, true); - - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); - }); - - it("should have `hasPremiumPersonally$` emit `false` when passing `false` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(false, false); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); - }); - - it("should have `hasPremiumFromAnyOrganization$` emit `false` when passing `false` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, false); - - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); - }); - - it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(true, false); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, true); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should have `canAccessPremium$` emit `false` when passing `false` for all arguments", async () => { - await sut.setHasPremium(false, false); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false); + expect(billingAccountProfileState.nextMock).toHaveBeenCalledWith([ + userId, + { hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false }, + ]); }); }); }); diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts index c6b6f104a8ee..336021c9930b 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts @@ -1,10 +1,10 @@ -import { map, Observable } from "rxjs"; +import { map, Observable, of, switchMap } from "rxjs"; import { ActiveUserState, - ActiveUserStateProvider, BILLING_DISK, KeyDefinition, + StateProvider, } from "../../../platform/state"; import { BillingAccountProfile, @@ -26,24 +26,34 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP hasPremiumPersonally$: Observable; hasPremiumFromAnySource$: Observable; - constructor(activeUserStateProvider: ActiveUserStateProvider) { - this.billingAccountProfileState = activeUserStateProvider.get( + constructor(stateProvider: StateProvider) { + this.billingAccountProfileState = stateProvider.getActive( BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, ); - this.hasPremiumFromAnyOrganization$ = this.billingAccountProfileState.state$.pipe( + // Setup an observable that will always track the currently active user + // but will fallback to emitting null when there is no active user. + const billingAccountProfileOrNull = stateProvider.activeUserId$.pipe( + switchMap((userId) => + userId != null + ? stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).state$ + : of(null), + ), + ); + + this.hasPremiumFromAnyOrganization$ = billingAccountProfileOrNull.pipe( map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization), ); - this.hasPremiumPersonally$ = this.billingAccountProfileState.state$.pipe( + this.hasPremiumPersonally$ = billingAccountProfileOrNull.pipe( map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally), ); - this.hasPremiumFromAnySource$ = this.billingAccountProfileState.state$.pipe( + this.hasPremiumFromAnySource$ = billingAccountProfileOrNull.pipe( map( (billingAccountProfile) => - billingAccountProfile?.hasPremiumFromAnyOrganization || - billingAccountProfile?.hasPremiumPersonally, + billingAccountProfile?.hasPremiumFromAnyOrganization === true || + billingAccountProfile?.hasPremiumPersonally === true, ), ); } From 1cb16543462eeba3111a7f8e2d28f3b4ebfd238c Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 26 Mar 2024 09:10:28 -0700 Subject: [PATCH 13/51] [PM-7087] Hide bulk assign collections menu item when showBulkAddToCollections is false (#8494) --- .../app/vault/components/vault-items/vault-items.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 66d4a559ceed..b17eed8ca113 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 @@ -91,7 +91,7 @@ export class VaultItemsComponent { } get bulkAssignToCollectionsAllowed() { - return this.ciphers.length > 0; + return this.showBulkAddToCollections && this.ciphers.length > 0; } protected canEditCollection(collection: CollectionView): boolean { From 2064862afcf05285d94ed0eefc1eeb99bdde4c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Tue, 26 Mar 2024 17:23:01 +0100 Subject: [PATCH 14/51] [PM-6832][PM-7030] Rollback macos runner version to 11 (#8450) --- .github/workflows/build-desktop.yml | 20 ++++++++++++++++---- .github/workflows/release-desktop-beta.yml | 15 ++++++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 2c28d0cb5236..e73f882bb40f 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -444,7 +444,10 @@ jobs: macos-build: name: MacOS Build - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} @@ -602,7 +605,10 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - browser-build - macos-build @@ -808,7 +814,10 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - browser-build - macos-build @@ -1006,7 +1015,10 @@ jobs: macos-package-dev: name: MacOS Package Dev Release Asset if: false # We need to look into how code signing works for dev - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - browser-build - macos-build diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 20bffb956ec4..b9e2d7a8c851 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -393,7 +393,10 @@ jobs: macos-build: name: MacOS Build - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} @@ -522,7 +525,10 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - setup - macos-build @@ -732,7 +738,10 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - setup - macos-build From 69530241d10519b517de984fb909cf8aae0aac6f Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 26 Mar 2024 13:00:43 -0400 Subject: [PATCH 15/51] [PM-6532] Admin Console Single Sign on Settings page fields expand too much (#8386) * added class to reduce width of fields * moved class to form --- bitwarden_license/bit-web/src/app/auth/sso/sso.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index 2c02e89e245f..72a073e0c04d 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -9,7 +9,7 @@ {{ "loading" | i18n }} -
+

{{ "ssoPolicyHelpStart" | i18n }} {{ "ssoPolicyHelpAnchor" | i18n }} From 1e75f24671d544f24efe6af792cc68b14206fbea Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 26 Mar 2024 10:29:50 -0700 Subject: [PATCH 16/51] [PM-7059] Use decryptedCollections$ observable instead of async getAllDecrypted call (#8488) --- apps/web/src/app/vault/individual-vault/vault.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6fee59d65bf4..1dc6fdaf1ca4 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -246,7 +246,7 @@ export class VaultComponent implements OnInit, OnDestroy { }); const filter$ = this.routedVaultFilterService.filter$; - const allCollections$ = Utils.asyncToObservable(() => this.collectionService.getAllDecrypted()); + const allCollections$ = this.collectionService.decryptedCollections$; const nestedCollections$ = allCollections$.pipe( map((collections) => getNestedCollectionTree(collections)), ); From 7f5583397427e6458f2f69b6f9fd995a32c6815d Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 26 Mar 2024 15:22:35 -0400 Subject: [PATCH 17/51] [AC-2285] Edit Unassigned Ciphers in AC Bug (#8410) * check if cipher is unassigned and call the proper service between cipherService get and apiService get. also check for custom user permissions --- apps/web/src/app/vault/org-vault/add-edit.component.ts | 10 ++++++++-- .../angular/src/vault/components/add-edit.component.ts | 8 ++++---- libs/common/src/vault/services/cipher.service.ts | 1 - 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index cb879dfcc757..ba0c65b10700 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -105,8 +105,14 @@ export class AddEditComponent extends BaseAddEditComponent { } protected async loadCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { - return await super.loadCipher(); + // Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin + const firstCipherCheck = await super.loadCipher(); + + if ( + !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + firstCipherCheck != null + ) { + return firstCipherCheck; } const response = await this.apiService.getCipherAdmin(this.cipherId); const data = new CipherData(response); diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 680672514a80..83131f8fc5bc 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -650,11 +650,11 @@ export class AddEditComponent implements OnInit, OnDestroy { protected saveCipher(cipher: Cipher) { const isNotClone = this.editMode && !this.cloneMode; - let orgAdmin = this.organization?.isAdmin; + let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); - if (this.flexibleCollectionsV1Enabled) { - // Flexible Collections V1 restricts admins, check the organization setting via canEditAllCiphers - orgAdmin = this.organization?.canEditAllCiphers(true); + // if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection + if (!cipher.collectionIds) { + orgAdmin = this.organization?.canEditAnyCollection; } return this.cipher.id == null diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4293e56728cc..829ee5ed4ee6 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1387,7 +1387,6 @@ export class CipherService implements CipherServiceAbstraction { cipher.attachments = attachments; }), ]); - return cipher; } From a66e224d3298a6600dc1c0acd0b6b37030c9e1e1 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 26 Mar 2024 18:41:14 -0400 Subject: [PATCH 18/51] Auth/PM-7072 - Token Service - Access Token Secure Storage Refactor (#8412) * PM-5263 - TokenSvc - WIP on access token secure storage refactor * PM-5263 - Add key generation svc to token svc. * PM-5263 - TokenSvc - more progress on encrypt access token work. * PM-5263 - TokenSvc TODO cleanup * PM-5263 - TokenSvc - rename * PM-5263 - TokenSvc - decryptAccess token must return null as that is a valid case. * PM-5263 - Add EncryptSvc dep to TokenSvc * PM-5263 - Add secure storage to token service * PM-5263 - TokenSvc - (1) Finish implementing accessTokenKey stored in secure storage + encrypted access token stored on disk (2) Remove no longer necessary migration flag as the presence of the accessTokenKey now serves the same purpose. Co-authored-by: Jake Fink * PM-5263 - TokenSvc - (1) Tweak return structure of decryptAccessToken to be more debuggable (2) Add TODO to add more error handling. * PM-5263 - TODO: update tests * PM-5263 - add temp logs * PM-5263 - TokenSvc - remove logs now that I don't need them. * fix tests for access token * PM-5263 - TokenSvc test cleanup - small tweaks / cleanup * PM-5263 - TokenService - per PR feedback from Justin - add error message to error message if possible. Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Jake Fink Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .../token-service.factory.ts | 20 +- .../browser/src/background/main.background.ts | 3 + apps/cli/src/bw.ts | 7 +- apps/desktop/src/main.ts | 29 +- .../illegal-secure-storage.service.ts | 28 ++ .../src/services/jslib-services.module.ts | 5 +- .../src/auth/services/token.service.spec.ts | 289 +++++++----------- .../common/src/auth/services/token.service.ts | 189 +++++++++--- .../src/auth/services/token.state.spec.ts | 2 - libs/common/src/auth/services/token.state.ts | 8 - 10 files changed, 353 insertions(+), 227 deletions(-) create mode 100644 apps/desktop/src/platform/services/illegal-secure-storage.service.ts diff --git a/apps/browser/src/auth/background/service-factories/token-service.factory.ts b/apps/browser/src/auth/background/service-factories/token-service.factory.ts index 25c30460f06d..ba42998209e4 100644 --- a/apps/browser/src/auth/background/service-factories/token-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/token-service.factory.ts @@ -1,6 +1,10 @@ import { TokenService as AbstractTokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; +import { + EncryptServiceInitOptions, + encryptServiceFactory, +} from "../../../platform/background/service-factories/encrypt-service.factory"; import { FactoryOptions, CachedServices, @@ -10,6 +14,14 @@ import { GlobalStateProviderInitOptions, globalStateProviderFactory, } from "../../../platform/background/service-factories/global-state-provider.factory"; +import { + KeyGenerationServiceInitOptions, + keyGenerationServiceFactory, +} from "../../../platform/background/service-factories/key-generation-service.factory"; +import { + LogServiceInitOptions, + logServiceFactory, +} from "../../../platform/background/service-factories/log-service.factory"; import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, @@ -29,7 +41,10 @@ export type TokenServiceInitOptions = TokenServiceFactoryOptions & SingleUserStateProviderInitOptions & GlobalStateProviderInitOptions & PlatformUtilsServiceInitOptions & - SecureStorageServiceInitOptions; + SecureStorageServiceInitOptions & + KeyGenerationServiceInitOptions & + EncryptServiceInitOptions & + LogServiceInitOptions; export function tokenServiceFactory( cache: { tokenService?: AbstractTokenService } & CachedServices, @@ -45,6 +60,9 @@ export function tokenServiceFactory( await globalStateProviderFactory(cache, opts), (await platformUtilsServiceFactory(cache, opts)).supportsSecureStorage(), await secureStorageServiceFactory(cache, opts), + await keyGenerationServiceFactory(cache, opts), + await encryptServiceFactory(cache, opts), + await logServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 452624d77e12..14ded13c3ec1 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -443,6 +443,9 @@ export default class MainBackground { this.globalStateProvider, this.platformUtilsService.supportsSecureStorage(), this.secureStorageService, + this.keyGenerationService, + this.encryptService, + this.logService, ); const migrationRunner = new MigrationRunner( diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 360ac6ffc480..e610f399541e 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -318,11 +318,16 @@ export class Main { this.accountService, ); + this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); + this.tokenService = new TokenService( this.singleUserStateProvider, this.globalStateProvider, this.platformUtilsService.supportsSecureStorage(), this.secureStorageService, + this.keyGenerationService, + this.encryptService, + this.logService, ); const migrationRunner = new MigrationRunner( @@ -343,8 +348,6 @@ export class Main { migrationRunner, ); - this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); - this.cryptoService = new CryptoService( this.keyGenerationService, this.cryptoFunctionService, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 5cb6abac58b9..67f08839c528 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,11 +6,15 @@ import { firstValueFrom } from "rxjs"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -45,6 +49,7 @@ import { ELECTRON_SUPPORTS_SECURE_STORAGE } from "./platform/services/electron-p import { ElectronStateService } from "./platform/services/electron-state.service"; import { ElectronStorageService } from "./platform/services/electron-storage.service"; import { I18nMainService } from "./platform/services/i18n.main.service"; +import { IllegalSecureStorageService } from "./platform/services/illegal-secure-storage.service"; import { ElectronMainMessagingService } from "./services/electron-main-messaging.service"; import { isMacAppStore } from "./utils"; @@ -62,6 +67,8 @@ export class Main { desktopSettingsService: DesktopSettingsService; migrationRunner: MigrationRunner; tokenService: TokenServiceAbstraction; + keyGenerationService: KeyGenerationServiceAbstraction; + encryptService: EncryptService; windowMain: WindowMain; messagingMain: MessagingMain; @@ -153,11 +160,28 @@ export class Main { this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); + this.mainCryptoFunctionService = new MainCryptoFunctionService(); + this.mainCryptoFunctionService.init(); + + this.keyGenerationService = new KeyGenerationService(this.mainCryptoFunctionService); + + this.encryptService = new EncryptServiceImplementation( + this.mainCryptoFunctionService, + this.logService, + true, // log mac failures + ); + + // Note: secure storage service is not available and should not be called in the main background process. + const illegalSecureStorageService = new IllegalSecureStorageService(); + this.tokenService = new TokenService( singleUserStateProvider, globalStateProvider, ELECTRON_SUPPORTS_SECURE_STORAGE, - this.storageService, + illegalSecureStorageService, + this.keyGenerationService, + this.encryptService, + this.logService, ); this.migrationRunner = new MigrationRunner( @@ -239,9 +263,6 @@ export class Main { this.clipboardMain = new ClipboardMain(); this.clipboardMain.init(); - - this.mainCryptoFunctionService = new MainCryptoFunctionService(); - this.mainCryptoFunctionService.init(); } bootstrap() { diff --git a/apps/desktop/src/platform/services/illegal-secure-storage.service.ts b/apps/desktop/src/platform/services/illegal-secure-storage.service.ts new file mode 100644 index 000000000000..12f86226bef0 --- /dev/null +++ b/apps/desktop/src/platform/services/illegal-secure-storage.service.ts @@ -0,0 +1,28 @@ +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; +import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; + +export class IllegalSecureStorageService implements AbstractStorageService { + constructor() {} + + get valuesRequireDeserialization(): boolean { + throw new Error("Method not implemented."); + } + has(key: string, options?: StorageOptions): Promise { + throw new Error("Method not implemented."); + } + save(key: string, obj: T, options?: StorageOptions): Promise { + throw new Error("Method not implemented."); + } + async get(key: string): Promise { + throw new Error("Method not implemented."); + } + async set(key: string, obj: T): Promise { + throw new Error("Method not implemented."); + } + async remove(key: string): Promise { + throw new Error("Method not implemented."); + } + async clear(): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index cab71631da85..b2aebe20f4b4 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -503,7 +503,10 @@ const typesafeProviders: Array = [ SingleUserStateProvider, GlobalStateProvider, SUPPORTS_SECURE_STORAGE, - AbstractStorageService, + SECURE_STORAGE, + KeyGenerationServiceAbstraction, + EncryptService, + LogService, ], }), safeProvider({ diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index a7b953f92806..63c581910a86 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -1,7 +1,10 @@ -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; import { StorageOptions } from "../../platform/models/domain/storage-options"; @@ -12,7 +15,6 @@ import { DecodedAccessToken, TokenService } from "./token.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, API_KEY_CLIENT_ID_DISK, API_KEY_CLIENT_ID_MEMORY, API_KEY_CLIENT_SECRET_DISK, @@ -28,7 +30,10 @@ describe("TokenService", () => { let singleUserStateProvider: FakeSingleUserStateProvider; let globalStateProvider: FakeGlobalStateProvider; - const secureStorageService = mock(); + let secureStorageService: MockProxy; + let keyGenerationService: MockProxy; + let encryptService: MockProxy; + let logService: MockProxy; const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut; const memoryVaultTimeout = 30; @@ -74,12 +79,19 @@ describe("TokenService", () => { userId: userIdFromAccessToken, }; + const accessTokenKeyB64 = { keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8" }; + beforeEach(() => { jest.clearAllMocks(); singleUserStateProvider = new FakeSingleUserStateProvider(); globalStateProvider = new FakeGlobalStateProvider(); + secureStorageService = mock(); + keyGenerationService = mock(); + encryptService = mock(); + logService = mock(); + const supportsSecureStorage = false; // default to false; tests will override as needed tokenService = createTokenService(supportsSecureStorage); }); @@ -89,8 +101,8 @@ describe("TokenService", () => { }); describe("Access Token methods", () => { - const accessTokenPartialSecureStorageKey = `_accessToken`; - const accessTokenSecureStorageKey = `${userIdFromAccessToken}${accessTokenPartialSecureStorageKey}`; + const accessTokenKeyPartialSecureStorageKey = `_accessTokenKey`; + const accessTokenKeySecureStorageKey = `${userIdFromAccessToken}${accessTokenKeyPartialSecureStorageKey}`; describe("setAccessToken", () => { it("should throw an error if the access token is null", async () => { @@ -150,18 +162,22 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should set the access token in secure storage, null out data on disk or in memory, and set a flag to indicate the token has been migrated", async () => { + it("should set an access token key in secure storage, the encrypted access token in disk, and clear out the token in memory", async () => { // Arrange: - // For testing purposes, let's assume that the access token is already in disk and memory - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - + // For testing purposes, let's assume that the access token is already in memory singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + keyGenerationService.createKey.mockResolvedValue("accessTokenKey" as any); + + const mockEncryptedAccessToken = "encryptedAccessToken"; + + encryptService.encrypt.mockResolvedValue({ + encryptedString: mockEncryptedAccessToken, + } as any); + // Act await tokenService.setAccessToken( accessTokenJwt, @@ -170,27 +186,22 @@ describe("TokenService", () => { ); // Assert - // assert that the access token was set in secure storage + // assert that the AccessTokenKey was set in secure storage expect(secureStorageService.save).toHaveBeenCalledWith( - accessTokenSecureStorageKey, - accessTokenJwt, + accessTokenKeySecureStorageKey, + "accessTokenKey", secureStorageOptions, ); - // assert data was migrated out of disk and memory + flag was set + // assert that the access token was encrypted and set in disk expect( singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock, - ).toHaveBeenCalledWith(null); + ).toHaveBeenCalledWith(mockEncryptedAccessToken); + + // assert data was migrated out of memory expect( singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock, ).toHaveBeenCalledWith(null); - - expect( - singleUserStateProvider.getFake( - userIdFromAccessToken, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, - ).nextMock, - ).toHaveBeenCalledWith(true); }); }); }); @@ -216,7 +227,13 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should get the access token from memory with no user id specified (uses global active user)", async () => { + test.each([ + [ + "should get the access token from memory for the provided user id", + userIdFromAccessToken, + ], + ["should get the access token from memory with no user id provided", undefined], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -228,37 +245,28 @@ describe("TokenService", () => { .stateSubject.next([userIdFromAccessToken, undefined]); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } // Act - const result = await tokenService.getAccessToken(); - - // Assert - expect(result).toEqual(accessTokenJwt); - }); - - it("should get the access token from memory for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - // set disk to undefined - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + const result = await tokenService.getAccessToken(userId); - // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); // Assert expect(result).toEqual(accessTokenJwt); }); }); describe("Disk storage tests (secure storage not supported on platform)", () => { - it("should get the access token from disk with no user id specified", async () => { + test.each([ + [ + "should get the access token from disk for the specified user id", + userIdFromAccessToken, + ], + ["should get the access token from disk with no user id specified", undefined], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -269,28 +277,14 @@ describe("TokenService", () => { .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - // Act - const result = await tokenService.getAccessToken(); - // Assert - expect(result).toEqual(accessTokenJwt); - }); - - it("should get the access token from disk for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); + const result = await tokenService.getAccessToken(userId); // Assert expect(result).toEqual(accessTokenJwt); }); @@ -302,7 +296,16 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should get the access token from secure storage when no user id is specified and the migration flag is set to true", async () => { + test.each([ + [ + "should get the encrypted access token from disk, decrypt it, and return it when user id is provided", + userIdFromAccessToken, + ], + [ + "should get the encrypted access token from disk, decrypt it, and return it when no user id is provided", + undefined, + ], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -310,76 +313,35 @@ describe("TokenService", () => { singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]); - secureStorageService.get.mockResolvedValue(accessTokenJwt); + secureStorageService.get.mockResolvedValue(accessTokenKeyB64); + encryptService.decryptToUtf8.mockResolvedValue("decryptedAccessToken"); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } // Act - const result = await tokenService.getAccessToken(); - // Assert - expect(result).toEqual(accessTokenJwt); - }); - - it("should get the access token from secure storage when user id is specified and the migration flag set to true", async () => { - // Arrange - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); + const result = await tokenService.getAccessToken(userId); - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); - - secureStorageService.get.mockResolvedValue(accessTokenJwt); - - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); - - // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); // Assert - expect(result).toEqual(accessTokenJwt); + expect(result).toEqual("decryptedAccessToken"); }); - it("should fallback and get the access token from disk when user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - // set access token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); - - // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); - - // Assert - expect(result).toEqual(accessTokenJwt); - - // assert that secure storage was not called - expect(secureStorageService.get).not.toHaveBeenCalled(); - }); - - it("should fallback and get the access token from disk when no user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => { + test.each([ + [ + "should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and a user id is provided", + userIdFromAccessToken, + ], + [ + "should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and no user id is provided", + undefined, + ], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -390,23 +352,19 @@ describe("TokenService", () => { .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } - // set access token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); + // No access token key set // Act - const result = await tokenService.getAccessToken(); + const result = await tokenService.getAccessToken(userId); // Assert expect(result).toEqual(accessTokenJwt); - - // assert that secure storage was not called - expect(secureStorageService.get).not.toHaveBeenCalled(); }); }); }); @@ -426,34 +384,16 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should clear the access token from all storage locations for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - // Act - await tokenService.clearAccessToken(userIdFromAccessToken); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock, - ).toHaveBeenCalledWith(null); - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock, - ).toHaveBeenCalledWith(null); - - expect(secureStorageService.remove).toHaveBeenCalledWith( - accessTokenSecureStorageKey, - secureStorageOptions, - ); - }); - - it("should clear the access token from all storage locations for the global active user", async () => { + test.each([ + [ + "should clear the access token from all storage locations for the provided user id", + userIdFromAccessToken, + ], + [ + "should clear the access token from all storage locations for the global active user", + undefined, + ], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -464,12 +404,14 @@ describe("TokenService", () => { .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } // Act - await tokenService.clearAccessToken(); + await tokenService.clearAccessToken(userIdFromAccessToken); // Assert expect( @@ -480,7 +422,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(null); expect(secureStorageService.remove).toHaveBeenCalledWith( - accessTokenSecureStorageKey, + accessTokenKeySecureStorageKey, secureStorageOptions, ); }); @@ -2232,6 +2174,9 @@ describe("TokenService", () => { globalStateProvider, supportsSecureStorage, secureStorageService, + keyGenerationService, + encryptService, + logService, ); } }); diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 4e9722614edf..a1dc7ecf21e2 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -1,11 +1,17 @@ import { firstValueFrom } from "rxjs"; +import { Opaque } from "type-fest"; import { decodeJwtTokenToJson } from "@bitwarden/auth/common"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; +import { EncString, EncryptedString } from "../../platform/models/domain/enc-string"; import { StorageOptions } from "../../platform/models/domain/storage-options"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { GlobalState, GlobalStateProvider, @@ -19,7 +25,6 @@ import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, API_KEY_CLIENT_ID_DISK, API_KEY_CLIENT_ID_MEMORY, API_KEY_CLIENT_SECRET_DISK, @@ -101,8 +106,14 @@ export type DecodedAccessToken = { jti?: string; }; +/** + * A symmetric key for encrypting the access token before the token is stored on disk. + * This key should be stored in secure storage. + * */ +type AccessTokenKey = Opaque; + export class TokenService implements TokenServiceAbstraction { - private readonly accessTokenSecureStorageKey: string = "_accessToken"; + private readonly accessTokenKeySecureStorageKey: string = "_accessTokenKey"; private readonly refreshTokenSecureStorageKey: string = "_refreshToken"; @@ -117,10 +128,17 @@ export class TokenService implements TokenServiceAbstraction { private globalStateProvider: GlobalStateProvider, private readonly platformSupportsSecureStorage: boolean, private secureStorageService: AbstractStorageService, + private keyGenerationService: KeyGenerationService, + private encryptService: EncryptService, + private logService: LogService, ) { this.initializeState(); } + // pivoting to an approach where we create a symmetric key we store in secure storage + // which is used to protect the data before persisting to disk. + // We will also use the same symmetric key to decrypt the data when reading from disk. + private initializeState(): void { this.emailTwoFactorTokenRecordGlobalState = this.globalStateProvider.get( EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, @@ -155,6 +173,84 @@ export class TokenService implements TokenServiceAbstraction { } } + private async getAccessTokenKey(userId: UserId): Promise { + const accessTokenKeyB64 = await this.secureStorageService.get< + ReturnType + >(`${userId}${this.accessTokenKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); + + if (!accessTokenKeyB64) { + return null; + } + + const accessTokenKey = SymmetricCryptoKey.fromJSON(accessTokenKeyB64) as AccessTokenKey; + return accessTokenKey; + } + + private async createAndSaveAccessTokenKey(userId: UserId): Promise { + const newAccessTokenKey = (await this.keyGenerationService.createKey(512)) as AccessTokenKey; + + await this.secureStorageService.save( + `${userId}${this.accessTokenKeySecureStorageKey}`, + newAccessTokenKey, + this.getSecureStorageOptions(userId), + ); + + return newAccessTokenKey; + } + + private async clearAccessTokenKey(userId: UserId): Promise { + await this.secureStorageService.remove( + `${userId}${this.accessTokenKeySecureStorageKey}`, + this.getSecureStorageOptions(userId), + ); + } + + private async getOrCreateAccessTokenKey(userId: UserId): Promise { + if (!this.platformSupportsSecureStorage) { + throw new Error("Platform does not support secure storage. Cannot obtain access token key."); + } + + if (!userId) { + throw new Error("User id not found. Cannot obtain access token key."); + } + + // First see if we have an accessTokenKey in secure storage and return it if we do + let accessTokenKey: AccessTokenKey = await this.getAccessTokenKey(userId); + + if (!accessTokenKey) { + // Otherwise, create a new one and save it to secure storage, then return it + accessTokenKey = await this.createAndSaveAccessTokenKey(userId); + } + + return accessTokenKey; + } + + private async encryptAccessToken(accessToken: string, userId: UserId): Promise { + const accessTokenKey = await this.getOrCreateAccessTokenKey(userId); + + return await this.encryptService.encrypt(accessToken, accessTokenKey); + } + + private async decryptAccessToken( + encryptedAccessToken: EncString, + userId: UserId, + ): Promise { + const accessTokenKey = await this.getAccessTokenKey(userId); + + if (!accessTokenKey) { + // If we don't have an accessTokenKey, then that means we don't have an access token as it hasn't been set yet + // and we have to return null here to properly indicate the the user isn't logged in. + return null; + } + + const decryptedAccessToken = await this.encryptService.decryptToUtf8( + encryptedAccessToken, + accessTokenKey, + ); + + return decryptedAccessToken; + } + /** * Internal helper for set access token which always requires user id. * This is useful because setTokens always will have a user id from the access token whereas @@ -173,26 +269,33 @@ export class TokenService implements TokenServiceAbstraction { ); switch (storageLocation) { - case TokenStorageLocation.SecureStorage: - await this.saveStringToSecureStorage(userId, this.accessTokenSecureStorageKey, accessToken); + case TokenStorageLocation.SecureStorage: { + // Secure storage implementations have variable length limitations (Windows), so we cannot + // store the access token directly. Instead, we encrypt with accessTokenKey and store that + // in secure storage. - // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 - // 2024-02-20: Remove access token from memory and disk so that we migrate to secure storage over time. - // Remove these 2 calls to remove the access token from memory and disk after 3 releases. + const encryptedAccessToken: EncString = await this.encryptAccessToken(accessToken, userId); - await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).update((_) => null); - await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); + // Save the encrypted access token to disk + await this.singleUserStateProvider + .get(userId, ACCESS_TOKEN_DISK) + .update((_) => encryptedAccessToken.encryptedString); - // Set flag to indicate that the access token has been migrated to secure storage (don't remove this) - await this.setAccessTokenMigratedToSecureStorage(userId); + // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 + // 2024-02-20: Remove access token from memory so that we migrate to encrypt the access token over time. + // Remove this call to remove the access token from memory after 3 releases. + await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); return; + } case TokenStorageLocation.Disk: + // Access token stored on disk unencrypted as platform does not support secure storage await this.singleUserStateProvider .get(userId, ACCESS_TOKEN_DISK) .update((_) => accessToken); return; case TokenStorageLocation.Memory: + // Access token stored in memory due to vault timeout settings await this.singleUserStateProvider .get(userId, ACCESS_TOKEN_MEMORY) .update((_) => accessToken); @@ -226,15 +329,14 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot clear access token."); } - // TODO: re-eval this once we get shared key definitions for vault timeout and vault timeout action data. + // TODO: re-eval this implementation once we get shared key definitions for vault timeout and vault timeout action data. // we can't determine storage location w/out vaultTimeoutAction and vaultTimeout - // but we can simply clear all locations to avoid the need to require those parameters + // but we can simply clear all locations to avoid the need to require those parameters. if (this.platformSupportsSecureStorage) { - await this.secureStorageService.remove( - `${userId}${this.accessTokenSecureStorageKey}`, - this.getSecureStorageOptions(userId), - ); + // Always clear the access token key when clearing the access token + // The next set of the access token will create a new access token key + await this.clearAccessTokenKey(userId); } // Platform doesn't support secure storage, so use state provider implementation @@ -249,36 +351,48 @@ export class TokenService implements TokenServiceAbstraction { return undefined; } - const accessTokenMigratedToSecureStorage = - await this.getAccessTokenMigratedToSecureStorage(userId); - if (this.platformSupportsSecureStorage && accessTokenMigratedToSecureStorage) { - return await this.getStringFromSecureStorage(userId, this.accessTokenSecureStorageKey); - } - // Try to get the access token from memory const accessTokenMemory = await this.getStateValueByUserIdAndKeyDef( userId, ACCESS_TOKEN_MEMORY, ); - if (accessTokenMemory != null) { return accessTokenMemory; } // If memory is null, read from disk - return await this.getStateValueByUserIdAndKeyDef(userId, ACCESS_TOKEN_DISK); - } + const accessTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, ACCESS_TOKEN_DISK); + if (!accessTokenDisk) { + return null; + } - private async getAccessTokenMigratedToSecureStorage(userId: UserId): Promise { - return await firstValueFrom( - this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE).state$, - ); - } + if (this.platformSupportsSecureStorage) { + const accessTokenKey = await this.getAccessTokenKey(userId); - private async setAccessTokenMigratedToSecureStorage(userId: UserId): Promise { - await this.singleUserStateProvider - .get(userId, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .update((_) => true); + if (!accessTokenKey) { + // We know this is an unencrypted access token because we don't have an access token key + return accessTokenDisk; + } + + try { + const encryptedAccessTokenEncString = new EncString(accessTokenDisk as EncryptedString); + + const decryptedAccessToken = await this.decryptAccessToken( + encryptedAccessTokenEncString, + userId, + ); + return decryptedAccessToken; + } catch (error) { + // If an error occurs during decryption, return null for logout. + // We don't try to recover here since we'd like to know + // if access token and key are getting out of sync. + this.logService.error( + `Failed to decrypt access token: ${error?.message ?? "Unknown error."}`, + ); + return null; + } + } + return accessTokenDisk; } // Private because we only ever set the refresh token when also setting the access token @@ -417,7 +531,7 @@ export class TokenService implements TokenServiceAbstraction { const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, - false, + false, // don't use secure storage for client id ); if (storageLocation === TokenStorageLocation.Disk) { @@ -484,7 +598,7 @@ export class TokenService implements TokenServiceAbstraction { const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, - false, + false, // don't use secure storage for client secret ); if (storageLocation === TokenStorageLocation.Disk) { @@ -567,6 +681,7 @@ export class TokenService implements TokenServiceAbstraction { }); } + // TODO: stop accepting optional userIds async clearTokens(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index f4089a73fb47..24eddc73f56c 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -3,7 +3,6 @@ import { KeyDefinition } from "../../platform/state"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, API_KEY_CLIENT_ID_DISK, API_KEY_CLIENT_ID_MEMORY, API_KEY_CLIENT_SECRET_DISK, @@ -17,7 +16,6 @@ import { describe.each([ [ACCESS_TOKEN_DISK, "accessTokenDisk"], [ACCESS_TOKEN_MEMORY, "accessTokenMemory"], - [ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, true], [REFRESH_TOKEN_DISK, "refreshTokenDisk"], [REFRESH_TOKEN_MEMORY, "refreshTokenMemory"], [REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, true], diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 022f56f7aa55..55471e1627a5 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -8,14 +8,6 @@ export const ACCESS_TOKEN_MEMORY = new KeyDefinition(TOKEN_MEMORY, "acce deserializer: (accessToken) => accessToken, }); -export const ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE = new KeyDefinition( - TOKEN_DISK, - "accessTokenMigratedToSecureStorage", - { - deserializer: (accessTokenMigratedToSecureStorage) => accessTokenMigratedToSecureStorage, - }, -); - export const REFRESH_TOKEN_DISK = new KeyDefinition(TOKEN_DISK, "refreshToken", { deserializer: (refreshToken) => refreshToken, }); From 98556ce8bd01b9303f245cd0058ab9e526cac726 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:43:52 +1000 Subject: [PATCH 19/51] [deps] AC: Update css-loader to v6.10.0 (#8473) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 83 ++++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9e050a22a8e..cf932a1363e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,7 +126,7 @@ "concurrently": "8.2.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", - "css-loader": "6.8.1", + "css-loader": "6.10.0", "electron": "28.2.8", "electron-builder": "24.13.3", "electron-log": "5.0.1", @@ -729,6 +729,32 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -17144,19 +17170,19 @@ } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">= 12.13.0" @@ -17166,7 +17192,48 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/postcss-modules-local-by-default": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-scope": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, "node_modules/css-select": { diff --git a/package.json b/package.json index e04d7139dc16..59d84a74274c 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "concurrently": "8.2.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", - "css-loader": "6.8.1", + "css-loader": "6.10.0", "electron": "28.2.8", "electron-builder": "24.13.3", "electron-log": "5.0.1", From 96d274b332f147e274053ed12c175de4cf57b9d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:53:34 +1000 Subject: [PATCH 20/51] [deps] AC: Update postcss-loader to v8 (#8480) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 31 ++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf932a1363e9..53687498534c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,7 +159,7 @@ "node-ipc": "9.2.1", "pkg": "5.8.1", "postcss": "8.4.35", - "postcss-loader": "7.3.4", + "postcss-loader": "8.1.1", "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.12", "process": "0.11.10", @@ -31501,25 +31501,34 @@ } }, "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, "dependencies": { - "cosmiconfig": "^8.3.5", + "cosmiconfig": "^9.0.0", "jiti": "^1.20.0", "semver": "^7.5.4" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/postcss-loader/node_modules/argparse": { @@ -31529,15 +31538,15 @@ "dev": true }, "node_modules/postcss-loader/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "dependencies": { + "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" diff --git a/package.json b/package.json index 59d84a74274c..86026c6e93e3 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "node-ipc": "9.2.1", "pkg": "5.8.1", "postcss": "8.4.35", - "postcss-loader": "7.3.4", + "postcss-loader": "8.1.1", "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.12", "process": "0.11.10", From 3f6a5671227d0a9e85c92c85bead575c57c2dbdb Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 27 Mar 2024 08:47:23 -0700 Subject: [PATCH 21/51] [AC-2351] Call filterCollections within the organizations$ subscription to avoid race condition (#8498) --- libs/angular/src/components/share.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 53f064d6f4a1..6687e784f014 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -62,6 +62,7 @@ export class ShareComponent implements OnInit, OnDestroy { this.organizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { if (this.organizationId == null && orgs.length > 0) { this.organizationId = orgs[0].id; + this.filterCollections(); } }); @@ -69,8 +70,6 @@ export class ShareComponent implements OnInit, OnDestroy { this.cipher = await cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain), ); - - this.filterCollections(); } filterCollections() { From e98d29d2c83ed5b9a77e51ccecad470a81eb73f1 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:34:15 -0400 Subject: [PATCH 22/51] [PM-5593] Removing BrowserSendService from services (#8512) * Removing send service from services, removed browser send, and pointed to send services * Make linter happy --------- Co-authored-by: Daniel James Smith --- .../browser/src/background/main.background.ts | 4 +- .../service-factories/send-service.factory.ts | 4 +- .../src/popup/services/services.module.ts | 42 ------------------- .../src/services/browser-send.service.ts | 15 ------- 4 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 apps/browser/src/services/browser-send.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 14ded13c3ec1..73c4356f698b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -143,6 +143,7 @@ import { } from "@bitwarden/common/tools/password-strength"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -213,7 +214,6 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; -import { BrowserSendService } from "../services/browser-send.service"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; @@ -698,7 +698,7 @@ export default class MainBackground { logoutCallback, ); this.containerService = new ContainerService(this.cryptoService, this.encryptService); - this.sendService = new BrowserSendService( + this.sendService = new SendService( this.cryptoService, this.i18nService, this.keyGenerationService, diff --git a/apps/browser/src/background/service-factories/send-service.factory.ts b/apps/browser/src/background/service-factories/send-service.factory.ts index bca46b47030c..7c64bc076af8 100644 --- a/apps/browser/src/background/service-factories/send-service.factory.ts +++ b/apps/browser/src/background/service-factories/send-service.factory.ts @@ -1,3 +1,4 @@ +import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { @@ -21,7 +22,6 @@ import { stateServiceFactory, StateServiceInitOptions, } from "../../platform/background/service-factories/state-service.factory"; -import { BrowserSendService } from "../../services/browser-send.service"; type SendServiceFactoryOptions = FactoryOptions; @@ -40,7 +40,7 @@ export function sendServiceFactory( "sendService", opts, async () => - new BrowserSendService( + new SendService( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 33fe6a52af68..25db6f78ca1a 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -16,7 +16,6 @@ import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -53,9 +52,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { FileUploadService } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService, LogService as LogServiceAbstraction, @@ -82,12 +79,6 @@ import { import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; -import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { - InternalSendService as InternalSendServiceAbstraction, - SendService, -} from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; @@ -115,7 +106,6 @@ import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; -import { BrowserSendService } from "../../services/browser-send.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultFilterService } from "../../vault/services/vault-filter.service"; @@ -295,38 +285,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("passwordGenerationService"), deps: [], }, - { - provide: SendService, - useFactory: ( - cryptoService: CryptoService, - i18nService: I18nServiceAbstraction, - keyGenerationService: KeyGenerationService, - stateServiceAbstraction: StateServiceAbstraction, - ) => { - return new BrowserSendService( - cryptoService, - i18nService, - keyGenerationService, - stateServiceAbstraction, - ); - }, - deps: [CryptoService, I18nServiceAbstraction, KeyGenerationService, StateServiceAbstraction], - }, - { - provide: InternalSendServiceAbstraction, - useExisting: SendService, - }, - { - provide: SendApiServiceAbstraction, - useFactory: ( - apiService: ApiService, - fileUploadService: FileUploadService, - sendService: InternalSendServiceAbstraction, - ) => { - return new SendApiService(apiService, fileUploadService, sendService); - }, - deps: [ApiService, FileUploadService, InternalSendServiceAbstraction], - }, { provide: SyncService, useFactory: getBgService("syncService"), deps: [] }, { provide: DomainSettingsService, diff --git a/apps/browser/src/services/browser-send.service.ts b/apps/browser/src/services/browser-send.service.ts deleted file mode 100644 index 8a197444a988..000000000000 --- a/apps/browser/src/services/browser-send.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { Send } from "@bitwarden/common/tools/send/models/domain/send"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service"; - -import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable"; - -@browserSession -export class BrowserSendService extends SendService { - @sessionSync({ initializer: Send.fromJSON, initializeAs: "array" }) - protected _sends: BehaviorSubject; - @sessionSync({ initializer: SendView.fromJSON, initializeAs: "array" }) - protected _sendViews: BehaviorSubject; -} From 14e8e34b2dc496c9617df6ec5de36d990e9e34a2 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Wed, 27 Mar 2024 12:35:13 -0400 Subject: [PATCH 23/51] Adjust scan permissions (#8513) --- .github/workflows/scan.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index ea9e69226adf..878171cd1722 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -10,8 +10,6 @@ on: pull_request_target: types: [opened, synchronize] -permissions: read-all - jobs: check-run: name: Check PR run @@ -22,6 +20,8 @@ jobs: runs-on: ubuntu-22.04 needs: check-run permissions: + contents: read + pull-requests: write security-events: write steps: @@ -43,7 +43,7 @@ jobs: additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 with: sarif_file: cx_result.sarif @@ -51,6 +51,9 @@ jobs: name: Quality scan runs-on: ubuntu-22.04 needs: check-run + permissions: + contents: read + pull-requests: write steps: - name: Check out repo From 64d6f6fef3a5cf4e1d505e9ac1f67862bf1b16d7 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 27 Mar 2024 18:02:56 +0100 Subject: [PATCH 24/51] Move export.component from @bitwarden/angular to @bitwarden/vault-export-ui (#8514) Move export.component Export from @bitwarden/vault-export-ui Fix imports on browser, desktop and web Co-authored-by: Daniel James Smith --- apps/browser/src/tools/popup/settings/export.component.ts | 2 +- apps/desktop/src/app/tools/export/export.component.ts | 2 +- apps/web/src/app/tools/vault-export/export.component.ts | 2 +- .../vault-export-ui/src}/components/export.component.ts | 3 +-- libs/tools/export/vault-export/vault-export-ui/src/index.ts | 1 + 5 files changed, 5 insertions(+), 5 deletions(-) rename libs/{angular/src/tools/export => tools/export/vault-export/vault-export-ui/src}/components/export.component.ts (98%) diff --git a/apps/browser/src/tools/popup/settings/export.component.ts b/apps/browser/src/tools/popup/settings/export.component.ts index 70735b5184d8..b62ed4c517f5 100644 --- a/apps/browser/src/tools/popup/settings/export.component.ts +++ b/apps/browser/src/tools/popup/settings/export.component.ts @@ -2,7 +2,6 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; -import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -13,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui"; @Component({ selector: "app-export", diff --git a/apps/desktop/src/app/tools/export/export.component.ts b/apps/desktop/src/app/tools/export/export.component.ts index 3a740122ebe6..80ae3c80f967 100644 --- a/apps/desktop/src/app/tools/export/export.component.ts +++ b/apps/desktop/src/app/tools/export/export.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; -import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -12,6 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui"; @Component({ selector: "app-export", diff --git a/apps/web/src/app/tools/vault-export/export.component.ts b/apps/web/src/app/tools/vault-export/export.component.ts index 8b14092f20e2..3f57f9aa71c8 100644 --- a/apps/web/src/app/tools/vault-export/export.component.ts +++ b/apps/web/src/app/tools/vault-export/export.component.ts @@ -1,7 +1,6 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; -import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -13,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui"; @Component({ selector: "app-export", diff --git a/libs/angular/src/tools/export/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts similarity index 98% rename from libs/angular/src/tools/export/components/export.component.ts rename to libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 400071e59cc8..ce478db19a73 100644 --- a/libs/angular/src/tools/export/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -2,6 +2,7 @@ import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@ import { UntypedFormBuilder, Validators } from "@angular/forms"; import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; +import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -18,8 +19,6 @@ import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-exp import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; -import { PasswordStrengthComponent } from "../../password-strength/password-strength.component"; - @Directive() export class ExportComponent implements OnInit, OnDestroy { @Output() onSaved = new EventEmitter(); diff --git a/libs/tools/export/vault-export/vault-export-ui/src/index.ts b/libs/tools/export/vault-export/vault-export-ui/src/index.ts index 4165ee4558a2..919bc8b38e5e 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/index.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/index.ts @@ -1 +1,2 @@ +export { ExportComponent } from "./components/export.component"; export { ExportScopeCalloutComponent } from "./components/export-scope-callout.component"; From 62ad39e697a617b07a7beb21081ea4db301703a7 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 27 Mar 2024 12:03:09 -0500 Subject: [PATCH 25/51] Ps/pm 5965/better config polling (#8325) * Create tracker that can await until expected observables are received. * Test dates are almost equal * Remove unused class method * Allow for updating active account in accout service fake * Correct observable tracker behavior Clarify documentation * Transition config service to state provider Updates the config fetching behavior to be lazy and ensure that any emitted value has been updated if older than a configurable value (statically compiled). If desired, config fetching can be ensured fresh through an async. * Update calls to config service in DI and bootstrapping * Migrate account server configs * Fix global config fetching * Test migration rollback * Adhere to implementation naming convention * Adhere to abstract class naming convention * Complete config abstraction rename * Remove unnecessary cli config service * Fix builds * Validate observable does not complete * Use token service to determine authed or unauthed config pull * Remove superfluous factory config * Name describe blocks after the thing they test * Remove implementation documentation Unfortunately the experience when linking to external documentation is quite poor. Instead of following the link and retrieving docs, you get a link that can be clicked to take you out of context to the docs. No link _does_ retrieve docs, but lacks indication in the implementation that documentation exists at all. On the balance, removing the link is the better experience. * Fix storybook --- apps/browser/src/auth/popup/sso.component.ts | 4 +- .../src/auth/popup/two-factor.component.ts | 4 +- .../browser/src/background/main.background.ts | 13 +- .../src/background/runtime.background.ts | 6 +- .../config-api.service.factory.ts | 10 +- .../config-service.factory.ts | 32 +- .../services/browser-config.service.ts | 38 -- .../src/popup/services/init.service.ts | 3 - .../src/popup/services/services.module.ts | 28 +- .../src/popup/settings/about.component.ts | 4 +- .../fileless-importer.background.spec.ts | 2 +- .../fileless-importer.background.ts | 4 +- .../components/vault/add-edit.component.ts | 4 +- apps/cli/src/bw.ts | 13 +- .../platform/services/cli-config.service.ts | 9 - apps/desktop/src/app/app.component.ts | 6 +- apps/desktop/src/app/services/init.service.ts | 4 - apps/desktop/src/auth/sso.component.ts | 4 +- apps/desktop/src/auth/two-factor.component.ts | 4 +- .../src/vault/app/vault/add-edit.component.ts | 4 +- .../core/services/group/group.service.ts | 6 +- .../core/services/user-admin.service.ts | 4 +- .../layouts/organization-layout.component.ts | 2 +- .../member-dialog/member-dialog.component.ts | 4 +- .../settings/account.component.ts | 4 +- apps/web/src/app/app.component.ts | 6 +- .../user-key-rotation.service.spec.ts | 6 +- .../key-rotation/user-key-rotation.service.ts | 4 +- .../emergency-add-edit-cipher.component.ts | 4 +- apps/web/src/app/auth/sso.component.ts | 4 +- apps/web/src/app/auth/two-factor.component.ts | 4 +- .../billing/shared/add-credit.component.ts | 4 +- apps/web/src/app/core/init.service.ts | 4 - .../src/app/layouts/user-layout.component.ts | 2 +- .../collection-dialog.component.ts | 4 +- .../vault-items/vault-items.stories.ts | 4 +- .../individual-vault/add-edit.component.ts | 4 +- .../vault-onboarding.component.spec.ts | 8 +- .../vault-onboarding.component.ts | 4 +- .../vault/individual-vault/vault.component.ts | 4 +- .../app/vault/org-vault/add-edit.component.ts | 4 +- ...-collection-assignment-dialog.component.ts | 4 +- .../app/vault/org-vault/vault.component.ts | 4 +- .../providers/providers-layout.component.ts | 2 +- .../providers/setup/setup.component.ts | 2 +- .../bit-web/src/app/auth/sso/sso.component.ts | 4 +- .../src/auth/components/sso.component.spec.ts | 26 +- .../src/auth/components/sso.component.ts | 4 +- .../components/two-factor.component.spec.ts | 8 +- .../auth/components/two-factor.component.ts | 4 +- .../directives/if-feature.directive.spec.ts | 8 +- .../src/directives/if-feature.directive.ts | 4 +- .../platform/guard/feature-flag.guard.spec.ts | 8 +- .../src/platform/guard/feature-flag.guard.ts | 4 +- .../src/services/jslib-services.module.ts | 27 +- .../vault/components/add-edit.component.ts | 4 +- .../spec/matchers/to-almost-equal.spec.ts | 54 +++ libs/common/spec/matchers/to-almost-equal.ts | 20 + libs/common/spec/observable-tracker.ts | 86 +++++ .../config/config-api.service.abstraction.ts | 6 +- .../config/config.service.abstraction.ts | 30 -- .../abstractions/config/config.service.ts | 47 +++ .../abstractions/config/server-config.ts | 5 - .../platform/abstractions/state.service.ts | 9 - .../src/platform/models/domain/account.ts | 3 - .../services/config/config-api.service.ts | 12 +- .../services/config/config.service.spec.ts | 360 +++++++++++------- .../services/config/config.service.ts | 130 ------- .../services/config/default-config.service.ts | 177 +++++++++ .../src/platform/services/state.service.ts | 18 - .../src/platform/state/state-definitions.ts | 3 + libs/common/src/state-migrations/migrate.ts | 7 +- .../49-move-account-server-configs.spec.ts | 112 ++++++ .../49-move-account-server-configs.ts | 51 +++ .../src/vault/services/cipher.service.spec.ts | 4 +- .../src/vault/services/cipher.service.ts | 4 +- .../fido2/fido2-client.service.spec.ts | 6 +- .../services/fido2/fido2-client.service.ts | 4 +- libs/common/test.setup.ts | 8 + 79 files changed, 946 insertions(+), 609 deletions(-) delete mode 100644 apps/browser/src/platform/services/browser-config.service.ts delete mode 100644 apps/cli/src/platform/services/cli-config.service.ts create mode 100644 libs/common/spec/matchers/to-almost-equal.spec.ts create mode 100644 libs/common/spec/matchers/to-almost-equal.ts create mode 100644 libs/common/spec/observable-tracker.ts delete mode 100644 libs/common/src/platform/abstractions/config/config.service.abstraction.ts create mode 100644 libs/common/src/platform/abstractions/config/config.service.ts delete mode 100644 libs/common/src/platform/services/config/config.service.ts create mode 100644 libs/common/src/platform/services/config/default-config.service.ts create mode 100644 libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 430bd855f1d0..228c7401fdab 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -12,7 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -44,7 +44,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService: EnvironmentService, logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, protected authService: AuthService, @Inject(WINDOW) private win: Window, ) { diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index da2c3482fddd..94dfb5155b14 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -16,7 +16,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -59,7 +59,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { appIdService: AppIdService, loginService: LoginService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, @Inject(WINDOW) protected win: Window, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 73c4356f698b..c2c8c5be724a 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -70,6 +70,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -93,6 +94,7 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -201,7 +203,6 @@ import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; -import { BrowserConfigService } from "../platform/services/browser-config.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; @@ -293,7 +294,7 @@ export default class MainBackground { avatarService: AvatarServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler; - configService: BrowserConfigService; + configService: ConfigService; configApiService: ConfigApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction; devicesService: DevicesServiceAbstraction; @@ -609,16 +610,13 @@ export default class MainBackground { this.userVerificationApiService = new UserVerificationApiService(this.apiService); - this.configApiService = new ConfigApiService(this.apiService, this.authService); + this.configApiService = new ConfigApiService(this.apiService, this.tokenService); - this.configService = new BrowserConfigService( - this.stateService, + this.configService = new DefaultConfigService( this.configApiService, - this.authService, this.environmentService, this.logService, this.stateProvider, - true, ); this.cipherService = new CipherService( @@ -1005,7 +1003,6 @@ export default class MainBackground { this.filelessImporterBackground.init(); await this.commandsBackground.init(); - this.configService.init(); this.twoFactorService.init(); await this.overlayBackground.init(); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 0a94e0a79a6c..dd55c14fb27f 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -3,7 +3,7 @@ import { firstValueFrom } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -46,7 +46,7 @@ export default class RuntimeBackground { private environmentService: BrowserEnvironmentService, private messagingService: MessagingService, private logService: LogService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private fido2Service: Fido2Service, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor @@ -136,7 +136,7 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(); }, 2000); - this.configService.triggerServerConfigFetch(); + await this.configService.ensureConfigFetched(); } break; case "openPopup": diff --git a/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts index c0dbf1f475db..3d7d508832ba 100644 --- a/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts @@ -2,9 +2,9 @@ import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstract import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { - authServiceFactory, - AuthServiceInitOptions, -} from "../../../auth/background/service-factories/auth-service.factory"; + tokenServiceFactory, + TokenServiceInitOptions, +} from "../../../auth/background/service-factories/token-service.factory"; import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; @@ -13,7 +13,7 @@ type ConfigApiServiceFactoyOptions = FactoryOptions; export type ConfigApiServiceInitOptions = ConfigApiServiceFactoyOptions & ApiServiceInitOptions & - AuthServiceInitOptions; + TokenServiceInitOptions; export function configApiServiceFactory( cache: { configApiService?: ConfigApiServiceAbstraction } & CachedServices, @@ -26,7 +26,7 @@ export function configApiServiceFactory( async () => new ConfigApiService( await apiServiceFactory(cache, opts), - await authServiceFactory(cache, opts), + await tokenServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/background/service-factories/config-service.factory.ts b/apps/browser/src/platform/background/service-factories/config-service.factory.ts index 4e31fb3141a5..a899f8fd9af9 100644 --- a/apps/browser/src/platform/background/service-factories/config-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/config-service.factory.ts @@ -1,10 +1,5 @@ -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; - -import { - authServiceFactory, - AuthServiceInitOptions, -} from "../../../auth/background/service-factories/auth-service.factory"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { configApiServiceFactory, ConfigApiServiceInitOptions } from "./config-api.service.factory"; import { @@ -13,39 +8,30 @@ import { } from "./environment-service.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; -import { stateProviderFactory } from "./state-provider.factory"; -import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; +import { stateProviderFactory, StateProviderInitOptions } from "./state-provider.factory"; -type ConfigServiceFactoryOptions = FactoryOptions & { - configServiceOptions?: { - subscribe?: boolean; - }; -}; +type ConfigServiceFactoryOptions = FactoryOptions; export type ConfigServiceInitOptions = ConfigServiceFactoryOptions & - StateServiceInitOptions & ConfigApiServiceInitOptions & - AuthServiceInitOptions & EnvironmentServiceInitOptions & - LogServiceInitOptions; + LogServiceInitOptions & + StateProviderInitOptions; export function configServiceFactory( - cache: { configService?: ConfigServiceAbstraction } & CachedServices, + cache: { configService?: ConfigService } & CachedServices, opts: ConfigServiceInitOptions, -): Promise { +): Promise { return factory( cache, "configService", opts, async () => - new ConfigService( - await stateServiceFactory(cache, opts), + new DefaultConfigService( await configApiServiceFactory(cache, opts), - await authServiceFactory(cache, opts), await environmentServiceFactory(cache, opts), await logServiceFactory(cache, opts), await stateProviderFactory(cache, opts), - opts.configServiceOptions?.subscribe ?? true, ), ); } diff --git a/apps/browser/src/platform/services/browser-config.service.ts b/apps/browser/src/platform/services/browser-config.service.ts deleted file mode 100644 index be8d087f3b65..000000000000 --- a/apps/browser/src/platform/services/browser-config.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ReplaySubject } from "rxjs"; - -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; -import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; -import { StateProvider } from "@bitwarden/common/platform/state"; - -import { browserSession, sessionSync } from "../decorators/session-sync-observable"; - -@browserSession -export class BrowserConfigService extends ConfigService { - @sessionSync({ initializer: ServerConfig.fromJSON }) - protected _serverConfig: ReplaySubject; - - constructor( - stateService: StateService, - configApiService: ConfigApiServiceAbstraction, - authService: AuthService, - environmentService: EnvironmentService, - logService: LogService, - stateProvider: StateProvider, - subscribe = false, - ) { - super( - stateService, - configApiService, - authService, - environmentService, - logService, - stateProvider, - subscribe, - ); - } -} diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index b0e80ab96067..4036ace31fd6 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -5,7 +5,6 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; @@ -19,7 +18,6 @@ export class InitService { private stateService: StateServiceAbstraction, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, - private configService: ConfigService, @Inject(DOCUMENT) private document: Document, ) {} @@ -55,7 +53,6 @@ export class InitService { this.logService.info("Force redraw is on"); } - this.configService.init(); this.setupVaultPopupHeartbeat(); }; } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 25db6f78ca1a..7ab04603e45e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -46,17 +46,13 @@ import { UserNotificationSettingsService, UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; -import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { - LogService, - LogService as LogServiceAbstraction, -} from "@bitwarden/common/platform/abstractions/log.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 { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -66,7 +62,6 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -95,7 +90,6 @@ import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; -import { BrowserConfigService } from "../../platform/services/browser-config.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; @@ -186,7 +180,7 @@ function getBgService(service: keyof MainBackground) { i18nService, ); }, - deps: [LogServiceAbstraction, I18nServiceAbstraction], + deps: [LogService, I18nServiceAbstraction], }, { provide: CipherFileUploadService, @@ -205,7 +199,7 @@ function getBgService(service: keyof MainBackground) { deps: [], }, { - provide: LogServiceAbstraction, + provide: LogService, useFactory: (platformUtilsService: PlatformUtilsService) => new ConsoleLogService(platformUtilsService.isDev()), deps: [PlatformUtilsService], @@ -367,7 +361,7 @@ function getBgService(service: keyof MainBackground) { storageService: AbstractStorageService, secureStorageService: AbstractStorageService, memoryStorageService: AbstractMemoryStorageService, - logService: LogServiceAbstraction, + logService: LogService, accountService: AccountServiceAbstraction, environmentService: EnvironmentService, tokenService: TokenService, @@ -389,7 +383,7 @@ function getBgService(service: keyof MainBackground) { AbstractStorageService, SECURE_STORAGE, MEMORY_STORAGE, - LogServiceAbstraction, + LogService, AccountServiceAbstraction, EnvironmentService, TokenService, @@ -430,18 +424,6 @@ function getBgService(service: keyof MainBackground) { }, deps: [PlatformUtilsService], }, - { - provide: ConfigService, - useClass: BrowserConfigService, - deps: [ - StateServiceAbstraction, - ConfigApiServiceAbstraction, - AuthServiceAbstraction, - EnvironmentService, - StateProvider, - LogService, - ], - }, { provide: FilePopoutUtilsService, useFactory: (platformUtilsService: PlatformUtilsService) => { diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/popup/settings/about.component.ts index 4cabb183aeeb..61b5749b5131 100644 --- a/apps/browser/src/popup/settings/about.component.ts +++ b/apps/browser/src/popup/settings/about.component.ts @@ -3,7 +3,7 @@ import { Component } from "@angular/core"; import { combineLatest, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { ButtonModule, DialogModule } from "@bitwarden/components"; @@ -24,7 +24,7 @@ export class AboutComponent { ]).pipe(map(([serverConfig, isCloud]) => ({ serverConfig, isCloud }))); constructor( - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private environmentService: EnvironmentService, ) {} } diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts index d3436099ef1e..858889b8874d 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.spec.ts @@ -4,7 +4,7 @@ import { firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index 3ddc7bd1b765..57c2faa930b1 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -5,7 +5,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ImportServiceAbstraction } from "@bitwarden/importer/core"; @@ -55,7 +55,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface * @param syncService - Used to trigger a full sync after the import is completed. */ constructor( - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private authService: AuthService, private policyService: PolicyService, private notificationBackground: NotificationBackground, diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index 8e52d44069b9..b27a98623112 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -11,7 +11,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve 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 { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -68,7 +68,7 @@ export class AddEditComponent extends BaseAddEditComponent { sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( cipherService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index e610f399541e..ce2152ffbf3f 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -47,6 +47,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { @@ -60,6 +61,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -131,7 +133,6 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; -import { CliConfigService } from "./platform/services/cli-config.service"; import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service"; import { ConsoleLogService } from "./platform/services/console-log.service"; import { I18nService } from "./platform/services/i18n.service"; @@ -214,7 +215,7 @@ export class Main { deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; authRequestService: AuthRequestService; configApiService: ConfigApiServiceAbstraction; - configService: CliConfigService; + configService: ConfigService; accountService: AccountService; globalStateProvider: GlobalStateProvider; singleUserStateProvider: SingleUserStateProvider; @@ -504,16 +505,13 @@ export class Main { this.stateService, ); - this.configApiService = new ConfigApiService(this.apiService, this.authService); + this.configApiService = new ConfigApiService(this.apiService, this.tokenService); - this.configService = new CliConfigService( - this.stateService, + this.configService = new DefaultConfigService( this.configApiService, - this.authService, this.environmentService, this.logService, this.stateProvider, - true, ); this.cipherService = new CipherService( @@ -714,7 +712,6 @@ export class Main { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); - this.configService.init(); const installedVersion = await this.stateService.getInstalledVersion(); const currentVersion = await this.platformUtilsService.getApplicationVersion(); diff --git a/apps/cli/src/platform/services/cli-config.service.ts b/apps/cli/src/platform/services/cli-config.service.ts deleted file mode 100644 index 6faa1b12e8a3..000000000000 --- a/apps/cli/src/platform/services/cli-config.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NEVER } from "rxjs"; - -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; - -export class CliConfigService extends ConfigService { - // The rxjs timer uses setTimeout/setInterval under the hood, which prevents the node process from exiting - // when the command is finished. Cli should never be alive long enough to use the timer, so we disable it. - protected refreshTimer$ = NEVER; -} diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index fa396ab313b4..196bebfcf74d 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -31,7 +31,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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"; @@ -147,7 +147,7 @@ export class AppComponent implements OnInit, OnDestroy { private modalService: ModalService, private keyConnectorService: KeyConnectorService, private userVerificationService: UserVerificationService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, @@ -265,7 +265,7 @@ export class AppComponent implements OnInit, OnDestroy { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.updateAppMenu(); - this.configService.triggerServerConfigFetch(); + await this.configService.ensureConfigFetched(); } break; case "openSettings": diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index f45d530eddf8..bb7d4e7b5215 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -11,7 +11,6 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -36,7 +35,6 @@ export class InitService { private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, private encryptService: EncryptService, - private configService: ConfigService, @Inject(DOCUMENT) private document: Document, ) {} @@ -70,8 +68,6 @@ export class InitService { const containerService = new ContainerService(this.cryptoService, this.encryptService); containerService.attachToGlobal(this.win); - - this.configService.init(); }; } } diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 0268133192f0..210319b9ed2a 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -8,7 +8,7 @@ import { } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -38,7 +38,7 @@ export class SsoComponent extends BaseSsoComponent { passwordGenerationService: PasswordGenerationServiceAbstraction, logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( ssoLoginService, diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index 9b862e7c9f57..8b46f3d1b9af 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -16,7 +16,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -59,7 +59,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { loginService: LoginService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, @Inject(WINDOW) protected win: Window, ) { super( diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index 8532b7462a8f..b89beebaa63f 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -8,7 +8,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve 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 { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -49,7 +49,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges, sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( cipherService, diff --git a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts b/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts index 33a3069e1dd0..63431cd6abe9 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts @@ -3,7 +3,7 @@ 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 { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CoreOrganizationModule } from "../../core-organization.module"; import { GroupView } from "../../views/group.view"; @@ -18,7 +18,7 @@ import { GroupDetailsResponse, GroupResponse } from "./responses/group.response" export class GroupService { constructor( protected apiService: ApiService, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) {} async get(orgId: string, groupId: string): Promise { @@ -52,7 +52,7 @@ export class GroupService { export class InternalGroupService extends GroupService { constructor( protected apiService: ApiService, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) { super(apiService, configService); } diff --git a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts index a1d1bc3e238e..399140e3ea60 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts @@ -6,7 +6,7 @@ import { OrganizationUserUpdateRequest, } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { OrganizationUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CoreOrganizationModule } from "../core-organization.module"; import { OrganizationUserAdminView } from "../views/organization-user-admin-view"; @@ -14,7 +14,7 @@ import { OrganizationUserAdminView } from "../views/organization-user-admin-view @Injectable({ providedIn: CoreOrganizationModule }) export class UserAdminService { constructor( - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private organizationUserService: OrganizationUserService, ) {} diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 90010160aa92..1924476327ec 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -17,7 +17,7 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; 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 668b09eb7ef3..752122de004c 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 @@ -24,7 +24,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; 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"; @@ -148,7 +148,7 @@ export class MemberDialogComponent implements OnDestroy { private userService: UserAdminService, private organizationUserService: OrganizationUserService, private dialogService: DialogService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private accountService: AccountService, organizationService: OrganizationService, ) { diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 8527aa1b1723..b218e680e377 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -11,7 +11,7 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request"; import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -95,7 +95,7 @@ export class AccountComponent { private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private formBuilder: FormBuilder, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit() { diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index a1b74566279a..23b45618c680 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -16,7 +16,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -83,7 +83,7 @@ export class AppComponent implements OnDestroy, OnInit { private policyService: InternalPolicyService, protected policyListService: PolicyListService, private keyConnectorService: KeyConnectorService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, @@ -158,7 +158,7 @@ export class AppComponent implements OnDestroy, OnInit { break; case "syncCompleted": if (message.successfully) { - this.configService.triggerServerConfigFetch(); + await this.configService.ensureConfigFetched(); } break; case "upgradeOrganization": { diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index 93ee85761751..7eabbbb5c19e 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -2,7 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; @@ -39,7 +39,7 @@ describe("KeyRotationService", () => { let mockCryptoService: MockProxy; let mockEncryptService: MockProxy; let mockStateService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; beforeAll(() => { mockApiService = mock(); @@ -52,7 +52,7 @@ describe("KeyRotationService", () => { mockCryptoService = mock(); mockEncryptService = mock(); mockStateService = mock(); - mockConfigService = mock(); + mockConfigService = mock(); keyRotationService = new UserKeyRotationService( mockApiService, diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index bb4c3494dd01..b53c71cb2e27 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -3,7 +3,7 @@ import { firstValueFrom } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -34,7 +34,7 @@ export class UserKeyRotationService { private cryptoService: CryptoService, private encryptService: EncryptService, private stateService: StateService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} /** diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts index d20f0cd1bd3e..9312ce5fc03d 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts @@ -6,7 +6,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -52,7 +52,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 2ef4f3eb155f..cdd979aa898e 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -13,7 +13,7 @@ import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-co import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -45,7 +45,7 @@ export class SsoComponent extends BaseSsoComponent { private orgDomainApiService: OrgDomainApiServiceAbstraction, private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( ssoLoginService, diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index a47a7a28487b..6760ab449faa 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -15,7 +15,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -49,7 +49,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest loginService: LoginService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, @Inject(WINDOW) protected win: Window, ) { super( diff --git a/apps/web/src/app/billing/shared/add-credit.component.ts b/apps/web/src/app/billing/shared/add-credit.component.ts index 25d49fac9e43..71050a9a6e10 100644 --- a/apps/web/src/app/billing/shared/add-credit.component.ts +++ b/apps/web/src/app/billing/shared/add-credit.component.ts @@ -13,7 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -57,7 +57,7 @@ export class AddCreditComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private organizationService: OrganizationService, private logService: LogService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) { const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; this.ppButtonFormAction = payPalConfig.buttonAction; diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index 60f3dea91513..d5576d3bf70f 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -10,7 +10,6 @@ import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/pla import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -28,7 +27,6 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, - private configService: ConfigService, @Inject(DOCUMENT) private document: Document, ) {} @@ -46,8 +44,6 @@ export class InitService { this.themingService.applyThemeChangesTo(this.document); const containerService = new ContainerService(this.cryptoService, this.encryptService); containerService.attachToGlobal(this.win); - - this.configService.init(); }; } } diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 2e1813697efd..ee30bed0d674 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -9,7 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; 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 357d2217e472..722ab972fcd3 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 @@ -18,7 +18,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -107,7 +107,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private organizationUserService: OrganizationUserService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, ) { 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 05659de073cd..ad80c9f4e581 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 @@ -8,7 +8,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -92,7 +92,7 @@ export default { } as Partial, }, { - provide: ConfigServiceAbstraction, + provide: ConfigService, useValue: { getFeatureFlag() { // does not currently affect any display logic, default all to OFF diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 8332b7e95f12..56f18c4a3bd6 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -9,7 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType, ProductType } from "@bitwarden/common/enums"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -65,7 +65,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, private billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts index 342471763058..8967336f758c 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts @@ -5,7 +5,7 @@ import { Subject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -21,7 +21,7 @@ describe("VaultOnboardingComponent", () => { let mockApiService: Partial; let mockPolicyService: MockProxy; let mockI18nService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; let mockVaultOnboardingService: MockProxy; let mockStateProvider: Partial; let setInstallExtLinkSpy: any; @@ -34,7 +34,7 @@ describe("VaultOnboardingComponent", () => { mockApiService = { getProfile: jest.fn(), }; - mockConfigService = mock(); + mockConfigService = mock(); mockVaultOnboardingService = mock(); mockStateProvider = { getActive: jest.fn().mockReturnValue( @@ -56,7 +56,7 @@ describe("VaultOnboardingComponent", () => { { provide: VaultOnboardingServiceAbstraction, useValue: mockVaultOnboardingService }, { provide: I18nService, useValue: mockI18nService }, { provide: ApiService, useValue: mockApiService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, { provide: StateProvider, useValue: mockStateProvider }, ], }).compileComponents(); diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 16f68d6111b3..dc3a41cf155f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -17,7 +17,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -55,7 +55,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, private apiService: ApiService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private vaultOnboardingService: VaultOnboardingServiceAbstraction, ) {} 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 1dc6fdaf1ca4..92d6e13020b5 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -41,7 +41,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -180,7 +180,7 @@ export class VaultComponent implements OnInit, OnDestroy { private eventCollectionService: EventCollectionService, private searchService: SearchService, private searchPipe: SearchPipe, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private apiService: ApiService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index ba0c65b10700..c4213989c6bc 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -7,7 +7,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -54,7 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent { sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index 04edce8543f9..091c6461780e 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -4,7 +4,7 @@ import { Subject } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; @@ -65,7 +65,7 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni private cipherService: CipherService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private organizationService: OrganizationService, ) {} 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 6691404b3dbb..028198723bf3 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -40,7 +40,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -184,7 +184,7 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) {} async ngOnInit() { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index 5f45379442be..b8afe1c235d0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -6,7 +6,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 35e3a8bad3f8..b3d3112bf5f0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -5,7 +5,7 @@ import { first } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index d5a1aebdd809..45cfc02a095f 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -26,7 +26,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -186,7 +186,7 @@ export class SsoComponent implements OnInit, OnDestroy { private i18nService: I18nService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit() { diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 82650cb7f189..c5c062d9a7cc 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -16,7 +16,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -66,7 +66,7 @@ describe("SsoComponent", () => { let mockPasswordGenerationService: MockProxy; let mockLogService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; // Mock authService.logIn params let code: string; @@ -107,16 +107,16 @@ describe("SsoComponent", () => { queryParams: mockQueryParams, } as any as ActivatedRoute; - mockSsoLoginService = mock(); - mockStateService = mock(); - mockPlatformUtilsService = mock(); - mockApiService = mock(); - mockCryptoFunctionService = mock(); - mockEnvironmentService = mock(); - mockPasswordGenerationService = mock(); - mockLogService = mock(); - mockUserDecryptionOptionsService = mock(); - mockConfigService = mock(); + mockSsoLoginService = mock(); + mockStateService = mock(); + mockPlatformUtilsService = mock(); + mockApiService = mock(); + mockCryptoFunctionService = mock(); + mockEnvironmentService = mock(); + mockPasswordGenerationService = mock(); + mockLogService = mock(); + mockUserDecryptionOptionsService = mock(); + mockConfigService = mock(); // Mock loginStrategyService.logIn params code = "code"; @@ -198,7 +198,7 @@ describe("SsoComponent", () => { useValue: mockUserDecryptionOptionsService, }, { provide: LogService, useValue: mockLogService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, ], }); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index f7d5504e082f..68d6e72e8d6c 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -15,7 +15,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -65,7 +65,7 @@ export class SsoComponent { protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) {} async ngOnInit() { diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index c27ba7082f00..9703c7e7030d 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -21,7 +21,7 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -62,7 +62,7 @@ describe("TwoFactorComponent", () => { let mockLoginService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -92,7 +92,7 @@ describe("TwoFactorComponent", () => { mockLoginService = mock(); mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); - mockConfigService = mock(); + mockConfigService = mock(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -169,7 +169,7 @@ describe("TwoFactorComponent", () => { useValue: mockUserDecryptionOptionsService, }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, ], }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 78d1c020b8cf..f64e591fa267 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -25,7 +25,7 @@ import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -91,7 +91,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected loginService: LoginService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); diff --git a/libs/angular/src/directives/if-feature.directive.spec.ts b/libs/angular/src/directives/if-feature.directive.spec.ts index 01364b2ada2f..944410be7d5c 100644 --- a/libs/angular/src/directives/if-feature.directive.spec.ts +++ b/libs/angular/src/directives/if-feature.directive.spec.ts @@ -4,7 +4,7 @@ import { By } from "@angular/platform-browser"; import { mock, MockProxy } from "jest-mock-extended"; import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { IfFeatureDirective } from "./if-feature.directive"; @@ -39,7 +39,7 @@ class TestComponent { describe("IfFeatureDirective", () => { let fixture: ComponentFixture; let content: HTMLElement; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => { mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) => @@ -51,14 +51,14 @@ describe("IfFeatureDirective", () => { fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement; beforeEach(async () => { - mockConfigService = mock(); + mockConfigService = mock(); await TestBed.configureTestingModule({ declarations: [IfFeatureDirective, TestComponent], providers: [ { provide: LogService, useValue: mock() }, { - provide: ConfigServiceAbstraction, + provide: ConfigService, useValue: mockConfigService, }, ], diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index ff0812567872..069f306a895f 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -1,7 +1,7 @@ import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; /** @@ -30,7 +30,7 @@ export class IfFeatureDirective implements OnInit { constructor( private templateRef: TemplateRef, private viewContainer: ViewContainerRef, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private logService: LogService, ) {} diff --git a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts index 95dd56cd50b2..88637dff978e 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts @@ -5,7 +5,7 @@ import { RouterTestingModule } from "@angular/router/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -21,11 +21,11 @@ describe("canAccessFeature", () => { const featureRoute = "enabled-feature"; const redirectRoute = "redirect"; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; let mockPlatformUtilsService: MockProxy; const setup = (featureGuard: CanActivateFn, flagValue: any) => { - mockConfigService = mock(); + mockConfigService = mock(); mockPlatformUtilsService = mock(); // Mock the correct getter based on the type of flagValue; also mock default values if one is not provided @@ -56,7 +56,7 @@ describe("canAccessFeature", () => { ]), ], providers: [ - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, { provide: LogService, useValue: mock() }, { diff --git a/libs/angular/src/platform/guard/feature-flag.guard.ts b/libs/angular/src/platform/guard/feature-flag.guard.ts index 8842f04152be..bfcabc2b53cc 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.ts @@ -2,7 +2,7 @@ import { inject } from "@angular/core"; import { CanActivateFn, Router } from "@angular/router"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -23,7 +23,7 @@ export const canAccessFeature = ( redirectUrlOnDisabled?: string, ): CanActivateFn => { return async () => { - const configService = inject(ConfigServiceAbstraction); + const configService = inject(ConfigService); const platformUtilsService = inject(PlatformUtilsService); const router = inject(Router); const i18nService = inject(I18nService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b2aebe20f4b4..67d38d33de0f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -111,7 +111,7 @@ import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -135,7 +135,7 @@ import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -400,7 +400,7 @@ const typesafeProviders: Array = [ autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) => new CipherService( cryptoService, @@ -424,7 +424,7 @@ const typesafeProviders: Array = [ AutofillSettingsServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, - ConfigServiceAbstraction, + ConfigService, ], }), safeProvider({ @@ -851,25 +851,18 @@ const typesafeProviders: Array = [ deps: [], }), safeProvider({ - provide: ConfigService, - useClass: ConfigService, - deps: [ - StateServiceAbstraction, - ConfigApiServiceAbstraction, - AuthServiceAbstraction, - EnvironmentService, - LogService, - StateProvider, - ], + provide: DefaultConfigService, + useClass: DefaultConfigService, + deps: [ConfigApiServiceAbstraction, EnvironmentService, LogService, StateProvider], }), safeProvider({ - provide: ConfigServiceAbstraction, - useExisting: ConfigService, + provide: ConfigService, + useExisting: DefaultConfigService, }), safeProvider({ provide: ConfigApiServiceAbstraction, useClass: ConfigApiService, - deps: [ApiServiceAbstraction, AuthServiceAbstraction], + deps: [ApiServiceAbstraction, TokenServiceAbstraction], }), safeProvider({ provide: AnonymousHubServiceAbstraction, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 83131f8fc5bc..4f5334d176bc 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -14,7 +14,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +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"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -119,7 +119,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected dialogService: DialogService, protected win: Window, protected datePipe: DatePipe, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) { this.typeOptions = [ { name: i18nService.t("typeLogin"), value: CipherType.Login }, diff --git a/libs/common/spec/matchers/to-almost-equal.spec.ts b/libs/common/spec/matchers/to-almost-equal.spec.ts new file mode 100644 index 000000000000..592254513728 --- /dev/null +++ b/libs/common/spec/matchers/to-almost-equal.spec.ts @@ -0,0 +1,54 @@ +describe("toAlmostEqual custom matcher", () => { + it("matches identical Dates", () => { + const date = new Date(); + expect(date).toAlmostEqual(date); + }); + + it("matches when older but within default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 5); + expect(date).toAlmostEqual(olderDate); + }); + + it("matches when newer but within default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 5); + expect(date).toAlmostEqual(olderDate); + }); + + it("doesn't match if older than default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 11); + expect(date).not.toAlmostEqual(olderDate); + }); + + it("doesn't match if newer than default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 11); + expect(date).not.toAlmostEqual(olderDate); + }); + + it("matches when older but within custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 15); + expect(date).toAlmostEqual(olderDate, 20); + }); + + it("matches when newer but within custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 15); + expect(date).toAlmostEqual(olderDate, 20); + }); + + it("doesn't match if older than custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 21); + expect(date).not.toAlmostEqual(olderDate, 20); + }); + + it("doesn't match if newer than custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 21); + expect(date).not.toAlmostEqual(olderDate, 20); + }); +}); diff --git a/libs/common/spec/matchers/to-almost-equal.ts b/libs/common/spec/matchers/to-almost-equal.ts new file mode 100644 index 000000000000..ba5aacc9b33d --- /dev/null +++ b/libs/common/spec/matchers/to-almost-equal.ts @@ -0,0 +1,20 @@ +/** + * Matches the expected date within an optional ms precision + * @param received The received date + * @param expected The expected date + * @param msPrecision The optional precision in milliseconds + */ +export const toAlmostEqual: jest.CustomMatcher = function ( + received: Date, + expected: Date, + msPrecision: number = 10, +) { + const receivedTime = received.getTime(); + const expectedTime = expected.getTime(); + const difference = Math.abs(receivedTime - expectedTime); + return { + pass: difference <= msPrecision, + message: () => + `expected ${received} to be within ${msPrecision}ms of ${expected} (actual difference: ${difference}ms)`, + }; +}; diff --git a/libs/common/spec/observable-tracker.ts b/libs/common/spec/observable-tracker.ts new file mode 100644 index 000000000000..a6f3e6a879fb --- /dev/null +++ b/libs/common/spec/observable-tracker.ts @@ -0,0 +1,86 @@ +import { Observable, Subscription, firstValueFrom, throwError, timeout } from "rxjs"; + +/** Test class to enable async awaiting of observable emissions */ +export class ObservableTracker { + private subscription: Subscription; + emissions: T[] = []; + constructor(private observable: Observable) { + this.emissions = this.trackEmissions(observable); + } + + /** Unsubscribes from the observable */ + unsubscribe() { + this.subscription.unsubscribe(); + } + + /** + * Awaits the next emission from the observable, or throws if the timeout is exceeded + * @param msTimeout The maximum time to wait for another emission before throwing + */ + async expectEmission(msTimeout = 50) { + await firstValueFrom( + this.observable.pipe( + timeout({ + first: msTimeout, + with: () => throwError(() => new Error("Timeout exceeded waiting for another emission.")), + }), + ), + ); + } + + /** Awaits until the the total number of emissions observed by this tracker equals or exceeds {@link count} + * @param count The number of emissions to wait for + */ + async pauseUntilReceived(count: number, msTimeout = 50): Promise { + for (let i = 0; i < count - this.emissions.length; i++) { + await this.expectEmission(msTimeout); + } + return this.emissions; + } + + private trackEmissions(observable: Observable): T[] { + const emissions: T[] = []; + this.subscription = observable.subscribe((value) => { + switch (value) { + case undefined: + case null: + emissions.push(value); + return; + default: + // process by type + break; + } + + switch (typeof value) { + case "string": + case "number": + case "boolean": + emissions.push(value); + break; + case "symbol": + // Cheating types to make symbols work at all + emissions.push(value.toString() as T); + break; + default: { + emissions.push(clone(value)); + } + } + }); + return emissions; + } +} +function clone(value: any): any { + if (global.structuredClone != undefined) { + return structuredClone(value); + } else { + return JSON.parse(JSON.stringify(value)); + } +} + +/** A test helper that builds an @see{@link ObservableTracker}, which can be used to assert things about the + * emissions of the given observable + * @param observable The observable to track + */ +export function subscribeTo(observable: Observable) { + return new ObservableTracker(observable); +} diff --git a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts index 2b25164e7cf6..63534becf343 100644 --- a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts @@ -1,5 +1,9 @@ +import { UserId } from "../../../types/guid"; import { ServerConfigResponse } from "../../models/response/server-config.response"; export abstract class ConfigApiServiceAbstraction { - get: () => Promise; + /** + * Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context. + */ + get: (userId: UserId | undefined) => Promise; } diff --git a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts deleted file mode 100644 index 1e1de9155f16..000000000000 --- a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Observable } from "rxjs"; -import { SemVer } from "semver"; - -import { FeatureFlag } from "../../../enums/feature-flag.enum"; -import { Region } from "../environment.service"; - -import { ServerConfig } from "./server-config"; - -export abstract class ConfigServiceAbstraction { - serverConfig$: Observable; - cloudRegion$: Observable; - getFeatureFlag$: ( - key: FeatureFlag, - defaultValue?: T, - ) => Observable; - getFeatureFlag: ( - key: FeatureFlag, - defaultValue?: T, - ) => Promise; - checkServerMeetsVersionRequirement$: ( - minimumRequiredServerVersion: SemVer, - ) => Observable; - - /** - * Force ConfigService to fetch an updated config from the server and emit it from serverConfig$ - * @deprecated The service implementation should subscribe to an observable and use that to trigger a new fetch from - * server instead - */ - triggerServerConfigFetch: () => void; -} diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts new file mode 100644 index 000000000000..9eca5891ac1e --- /dev/null +++ b/libs/common/src/platform/abstractions/config/config.service.ts @@ -0,0 +1,47 @@ +import { Observable } from "rxjs"; +import { SemVer } from "semver"; + +import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { Region } from "../environment.service"; + +import { ServerConfig } from "./server-config"; + +export abstract class ConfigService { + /** The server config of the currently active user */ + serverConfig$: Observable; + /** The cloud region of the currently active user */ + cloudRegion$: Observable; + /** + * Retrieves the value of a feature flag for the currently active user + * @param key The feature flag to retrieve + * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable + * @returns An observable that emits the value of the feature flag, updates as the server config changes + */ + getFeatureFlag$: ( + key: FeatureFlag, + defaultValue?: T, + ) => Observable; + /** + * Retrieves the value of a feature flag for the currently active user + * @param key The feature flag to retrieve + * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable + * @returns The value of the feature flag + */ + getFeatureFlag: ( + key: FeatureFlag, + defaultValue?: T, + ) => Promise; + /** + * Verifies whether the server version meets the minimum required version + * @param minimumRequiredServerVersion The minimum version required + * @returns True if the server version is greater than or equal to the minimum required version + */ + checkServerMeetsVersionRequirement$: ( + minimumRequiredServerVersion: SemVer, + ) => Observable; + + /** + * Triggers a check that the config for the currently active user is up-to-date. If it is not, it will be fetched from the server and stored. + */ + abstract ensureConfigFetched(): Promise; +} diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index 2fa250202e43..287e359f189f 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -7,7 +7,6 @@ import { } from "../../models/data/server-config.data"; const dayInMilliseconds = 24 * 3600 * 1000; -const eighteenHoursInMilliseconds = 18 * 3600 * 1000; export class ServerConfig { version: string; @@ -38,10 +37,6 @@ export class ServerConfig { return this.getAgeInMilliseconds() <= dayInMilliseconds; } - expiresSoon(): boolean { - return this.getAgeInMilliseconds() >= eighteenHoursInMilliseconds; - } - static fromJSON(obj: Jsonify): ServerConfig { if (obj == null) { return null; diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 514689313f5d..b4847279c33d 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -16,7 +16,6 @@ import { LocalData } from "../../vault/models/data/local.data"; import { CipherView } from "../../vault/models/view/cipher.view"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { KdfType } from "../enums"; -import { ServerConfigData } from "../models/data/server-config.data"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; @@ -278,14 +277,6 @@ export abstract class StateService { setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; getApproveLoginRequests: (options?: StorageOptions) => Promise; setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use ConfigService - */ - getServerConfig: (options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use ConfigService - */ - setServerConfig: (value: ServerConfigData, options?: StorageOptions) => Promise; /** * fetches string value of URL user tried to navigate to while unauthenticated. * @param options Defines the storage options for the URL; Defaults to session Storage. diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 2657467ae6af..d01e9d5b8df3 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -18,7 +18,6 @@ import { CipherView } from "../../../vault/models/view/cipher.view"; import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info"; import { KdfType } from "../../enums"; import { Utils } from "../../misc/utils"; -import { ServerConfigData } from "../../models/data/server-config.data"; import { EncryptedString, EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; @@ -196,7 +195,6 @@ export class AccountSettings { protectedPin?: string; vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; - serverConfig?: ServerConfigData; approveLoginRequests?: boolean; avatarColor?: string; trustDeviceChoiceForDecryption?: boolean; @@ -214,7 +212,6 @@ export class AccountSettings { obj?.pinProtected, EncString.fromJSON, ), - serverConfig: ServerConfigData.fromJSON(obj?.serverConfig), }); } } diff --git a/libs/common/src/platform/services/config/config-api.service.ts b/libs/common/src/platform/services/config/config-api.service.ts index 702c38f53cfe..f283410acea6 100644 --- a/libs/common/src/platform/services/config/config-api.service.ts +++ b/libs/common/src/platform/services/config/config-api.service.ts @@ -1,18 +1,20 @@ import { ApiService } from "../../../abstractions/api.service"; -import { AuthService } from "../../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { TokenService } from "../../../auth/abstractions/token.service"; +import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ServerConfigResponse } from "../../models/response/server-config.response"; export class ConfigApiService implements ConfigApiServiceAbstraction { constructor( private apiService: ApiService, - private authService: AuthService, + private tokenService: TokenService, ) {} - async get(): Promise { + async get(userId: UserId | undefined): Promise { + // Authentication adds extra context to config responses, if the user has an access token, we want to use it + // We don't particularly care about ensuring the token is valid and not expired, just that it exists const authed: boolean = - (await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut; + userId == null ? false : (await this.tokenService.getAccessToken(userId)) != null; const r = await this.apiService.send("GET", "/config", null, authed, true); return new ServerConfigResponse(r); diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index 7f337f33224a..d643311a26fd 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -1,200 +1,264 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { ReplaySubject, skip, take } from "rxjs"; +/** + * need to update test environment so structuredClone works appropriately + * @jest-environment ../../libs/shared/test.environment.ts + */ -import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { AuthService } from "../../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { mock } from "jest-mock-extended"; +import { Subject, firstValueFrom, of } from "rxjs"; + +import { + FakeGlobalState, + FakeSingleUserState, + FakeStateProvider, + awaitAsync, + mockAccountServiceWith, +} from "../../../../spec"; +import { subscribeTo } from "../../../../spec/observable-tracker"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; -import { StateService } from "../../abstractions/state.service"; +import { Utils } from "../../misc/utils"; import { ServerConfigData } from "../../models/data/server-config.data"; import { EnvironmentServerConfigResponse, ServerConfigResponse, ThirdPartyServerConfigResponse, } from "../../models/response/server-config.response"; -import { StateProvider } from "../../state"; -import { ConfigService } from "./config.service"; +import { + ApiUrl, + DefaultConfigService, + RETRIEVAL_INTERVAL, + GLOBAL_SERVER_CONFIGURATIONS, + USER_SERVER_CONFIG, +} from "./default-config.service"; describe("ConfigService", () => { - let stateService: MockProxy; - let configApiService: MockProxy; - let authService: MockProxy; - let environmentService: MockProxy; - let logService: MockProxy; - let replaySubject: ReplaySubject; - let stateProvider: StateProvider; - - let serverResponseCount: number; // increments to track distinct responses received from server - - // Observables will start emitting as soon as this is created, so only create it - // after everything is mocked - const configServiceFactory = () => { - const configService = new ConfigService( - stateService, - configApiService, - authService, - environmentService, - logService, - stateProvider, - ); - configService.init(); - return configService; - }; + const configApiService = mock(); + const environmentService = mock(); + const logService = mock(); + let stateProvider: FakeStateProvider; + let globalState: FakeGlobalState>; + let userState: FakeSingleUserState; + const activeApiUrl = apiUrl(0); + const userId = "userId" as UserId; + const accountService = mockAccountServiceWith(userId); + const tooOld = new Date(Date.now() - 1.1 * RETRIEVAL_INTERVAL); beforeEach(() => { - stateService = mock(); - configApiService = mock(); - authService = mock(); - environmentService = mock(); - logService = mock(); - replaySubject = new ReplaySubject(1); - const accountService = mockAccountServiceWith("0" as UserId); stateProvider = new FakeStateProvider(accountService); - - environmentService.environment$ = replaySubject.asObservable(); - - serverResponseCount = 1; - configApiService.get.mockImplementation(() => - Promise.resolve(serverConfigResponseFactory("server" + serverResponseCount++)), - ); - - jest.useFakeTimers(); + globalState = stateProvider.global.getFake(GLOBAL_SERVER_CONFIGURATIONS); + userState = stateProvider.singleUser.getFake(userId, USER_SERVER_CONFIG); }); afterEach(() => { - jest.useRealTimers(); + jest.resetAllMocks(); }); - it("Uses storage as fallback", (done) => { - const storedConfigData = serverConfigDataFactory("storedConfig"); - stateService.getServerConfig.mockResolvedValueOnce(storedConfigData); + describe.each([null, userId])("active user: %s", (activeUserId) => { + let sut: DefaultConfigService; - configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch")); - - const configService = configServiceFactory(); + beforeAll(async () => { + await accountService.switchAccount(activeUserId); + }); - configService.serverConfig$.pipe(take(1)).subscribe((config) => { - expect(config).toEqual(new ServerConfig(storedConfigData)); - expect(stateService.getServerConfig).toHaveBeenCalledTimes(1); - expect(stateService.setServerConfig).not.toHaveBeenCalled(); - done(); + beforeEach(() => { + environmentService.environment$ = of(environmentFactory(activeApiUrl)); + sut = new DefaultConfigService( + configApiService, + environmentService, + logService, + stateProvider, + ); }); - configService.triggerServerConfigFetch(); - }); + describe("serverConfig$", () => { + it.each([{}, null])("handles null stored state", async (globalTestState) => { + globalState.stateSubject.next(globalTestState); + userState.nextState(null); + await expect(firstValueFrom(sut.serverConfig$)).resolves.not.toThrow(); + }); - it("Stream does not error out if fetch fails", (done) => { - const storedConfigData = serverConfigDataFactory("storedConfig"); - stateService.getServerConfig.mockResolvedValueOnce(storedConfigData); + describe.each(["stale", "missing"])("%s config", (configStateDescription) => { + const userStored = + configStateDescription === "missing" + ? null + : serverConfigFactory(activeApiUrl + userId, tooOld); + const globalStored = + configStateDescription === "missing" + ? {} + : { + [activeApiUrl]: serverConfigFactory(activeApiUrl, tooOld), + }; + + beforeEach(() => { + globalState.stateSubject.next(globalStored); + userState.nextState(userStored); + }); - const configService = configServiceFactory(); + // sanity check + test("authed and unauthorized state are different", () => { + expect(globalStored[activeApiUrl]).not.toEqual(userStored); + }); - configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server1"); - done(); - } catch (e) { - done(e); - } - }); + describe("fail to fetch", () => { + beforeEach(() => { + configApiService.get.mockRejectedValue(new Error("Unable to fetch")); + }); + + it("uses storage as fallback", async () => { + const actual = await firstValueFrom(sut.serverConfig$); + expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); + expect(configApiService.get).toHaveBeenCalledTimes(1); + }); + + it("does not error out when fetch fails", async () => { + await expect(firstValueFrom(sut.serverConfig$)).resolves.not.toThrow(); + expect(configApiService.get).toHaveBeenCalledTimes(1); + }); + + it("logs an error when unable to fetch", async () => { + await firstValueFrom(sut.serverConfig$); + + expect(logService.error).toHaveBeenCalledWith( + `Unable to fetch ServerConfig from ${activeApiUrl}: Unable to fetch`, + ); + }); + }); - configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch")); - configService.triggerServerConfigFetch(); + describe("fetch success", () => { + const response = serverConfigResponseFactory(); + const newConfig = new ServerConfig(new ServerConfigData(response)); - configApiService.get.mockResolvedValueOnce(serverConfigResponseFactory("server1")); - configService.triggerServerConfigFetch(); - }); + it("should be a new config", async () => { + expect(newConfig).not.toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); + }); - describe("Fetches config from server", () => { - beforeEach(() => { - stateService.getServerConfig.mockResolvedValueOnce(null); - }); + it("fetches config from server when it's older than an hour", async () => { + await firstValueFrom(sut.serverConfig$); - it.each([1, 2, 3])( - "after %p hour/s", - (hours: number, done: jest.DoneCallback) => { - const configService = configServiceFactory(); - - // skip previous hours (if any) - configService.serverConfig$.pipe(skip(hours - 1), take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server" + hours); - expect(configApiService.get).toHaveBeenCalledTimes(hours); - done(); - } catch (e) { - done(e); - } - }); + expect(configApiService.get).toHaveBeenCalledTimes(1); + }); - const oneHourInMs = 1000 * 3600; - jest.advanceTimersByTime(oneHourInMs * hours + 1); - }, - ); - - it("when environment URLs change", (done) => { - const configService = configServiceFactory(); - - configService.serverConfig$.pipe(take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server1"); - done(); - } catch (e) { - done(e); - } - }); + it("returns the updated config", async () => { + configApiService.get.mockResolvedValue(response); - replaySubject.next(null); - }); + const actual = await firstValueFrom(sut.serverConfig$); - it("when triggerServerConfigFetch() is called", (done) => { - const configService = configServiceFactory(); + // This is the time the response is converted to a config + expect(actual.utcDate).toAlmostEqual(newConfig.utcDate, 1000); + delete actual.utcDate; + delete newConfig.utcDate; - configService.serverConfig$.pipe(take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server1"); - done(); - } catch (e) { - done(e); - } + expect(actual).toEqual(newConfig); + }); + }); }); - configService.triggerServerConfigFetch(); + describe("fresh configuration", () => { + const userStored = serverConfigFactory(activeApiUrl + userId); + const globalStored = { + [activeApiUrl]: serverConfigFactory(activeApiUrl), + }; + beforeEach(() => { + globalState.stateSubject.next(globalStored); + userState.nextState(userStored); + }); + it("does not fetch from server", async () => { + await firstValueFrom(sut.serverConfig$); + + expect(configApiService.get).not.toHaveBeenCalled(); + }); + + it("uses stored value", async () => { + const actual = await firstValueFrom(sut.serverConfig$); + expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); + }); + + it("does not complete after emit", async () => { + const emissions = []; + const subscription = sut.serverConfig$.subscribe((v) => emissions.push(v)); + await awaitAsync(); + expect(emissions.length).toBe(1); + expect(subscription.closed).toBe(false); + }); + }); }); }); - it("Saves server config to storage when the user is logged in", (done) => { - stateService.getServerConfig.mockResolvedValueOnce(null); - authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked); - const configService = configServiceFactory(); - - configService.serverConfig$.pipe(take(1)).subscribe(() => { - try { - expect(stateService.setServerConfig).toHaveBeenCalledWith( - expect.objectContaining({ gitHash: "server1" }), - ); - done(); - } catch (e) { - done(e); - } + describe("environment change", () => { + let sut: DefaultConfigService; + let environmentSubject: Subject; + + beforeAll(async () => { + // updating environment with an active account is undefined behavior + await accountService.switchAccount(null); + }); + + beforeEach(() => { + environmentSubject = new Subject(); + environmentService.environment$ = environmentSubject; + sut = new DefaultConfigService( + configApiService, + environmentService, + logService, + stateProvider, + ); }); - configService.triggerServerConfigFetch(); + describe("serverConfig$", () => { + it("emits a new config when the environment changes", async () => { + const globalStored = { + [apiUrl(0)]: serverConfigFactory(apiUrl(0)), + [apiUrl(1)]: serverConfigFactory(apiUrl(1)), + }; + globalState.stateSubject.next(globalStored); + + const spy = subscribeTo(sut.serverConfig$); + + environmentSubject.next(environmentFactory(apiUrl(0))); + environmentSubject.next(environmentFactory(apiUrl(1))); + + const expected = [globalStored[apiUrl(0)], globalStored[apiUrl(1)]]; + + const actual = await spy.pauseUntilReceived(2); + expect(actual.length).toBe(2); + + // validate dates this is done separately because the dates are created when ServerConfig is initialized + expect(actual[0].utcDate).toAlmostEqual(expected[0].utcDate, 1000); + expect(actual[1].utcDate).toAlmostEqual(expected[1].utcDate, 1000); + delete actual[0].utcDate; + delete actual[1].utcDate; + delete expected[0].utcDate; + delete expected[1].utcDate; + + expect(actual).toEqual(expected); + spy.unsubscribe(); + }); + }); }); }); -function serverConfigDataFactory(gitHash: string) { - return new ServerConfigData(serverConfigResponseFactory(gitHash)); +function apiUrl(count: number) { + return `https://api${count}.test.com`; } -function serverConfigResponseFactory(gitHash: string) { +function serverConfigFactory(hash: string, date: Date = new Date()) { + const config = new ServerConfig(serverConfigDataFactory(hash)); + config.utcDate = date; + return config; +} + +function serverConfigDataFactory(hash?: string) { + return new ServerConfigData(serverConfigResponseFactory(hash)); +} + +function serverConfigResponseFactory(hash?: string) { return new ServerConfigResponse({ version: "myConfigVersion", - gitHash: gitHash, + gitHash: hash ?? Utils.newGuid(), // Use optional git hash to store uniqueness value server: new ThirdPartyServerConfigResponse({ name: "myThirdPartyServer", url: "www.example.com", @@ -209,3 +273,9 @@ function serverConfigResponseFactory(gitHash: string) { }, }); } + +function environmentFactory(apiUrl: string) { + return { + getApiUrl: () => apiUrl, + } as Environment; +} diff --git a/libs/common/src/platform/services/config/config.service.ts b/libs/common/src/platform/services/config/config.service.ts deleted file mode 100644 index 86948fc1c0e8..000000000000 --- a/libs/common/src/platform/services/config/config.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - ReplaySubject, - Subject, - catchError, - concatMap, - defer, - delayWhen, - firstValueFrom, - map, - merge, - timer, -} from "rxjs"; -import { SemVer } from "semver"; - -import { AuthService } from "../../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; -import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; -import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; -import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction"; -import { ServerConfig } from "../../abstractions/config/server-config"; -import { EnvironmentService, Region } from "../../abstractions/environment.service"; -import { LogService } from "../../abstractions/log.service"; -import { StateService } from "../../abstractions/state.service"; -import { ServerConfigData } from "../../models/data/server-config.data"; -import { StateProvider } from "../../state"; - -const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600; - -export class ConfigService implements ConfigServiceAbstraction { - private inited = false; - - protected _serverConfig = new ReplaySubject(1); - serverConfig$ = this._serverConfig.asObservable(); - - private _forceFetchConfig = new Subject(); - protected refreshTimer$ = timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS); // after 1 hour, then every hour - - cloudRegion$ = this.serverConfig$.pipe( - map((config) => config?.environment?.cloudRegion ?? Region.US), - ); - - constructor( - private stateService: StateService, - private configApiService: ConfigApiServiceAbstraction, - private authService: AuthService, - private environmentService: EnvironmentService, - private logService: LogService, - private stateProvider: StateProvider, - - // Used to avoid duplicate subscriptions, e.g. in browser between the background and popup - private subscribe = true, - ) {} - - init() { - if (!this.subscribe || this.inited) { - return; - } - - const latestServerConfig$ = defer(() => this.configApiService.get()).pipe( - map((response) => new ServerConfigData(response)), - delayWhen((data) => this.saveConfig(data)), - catchError((e: unknown) => { - // fall back to stored ServerConfig (if any) - this.logService.error("Unable to fetch ServerConfig: " + (e as Error)?.message); - return this.stateService.getServerConfig(); - }), - ); - - // If you need to fetch a new config when an event occurs, add an observable that emits on that event here - merge( - this.refreshTimer$, // an overridable interval - this.environmentService.environment$, // when environment URLs change (including when app is started) - this._forceFetchConfig, // manual - ) - .pipe( - concatMap(() => latestServerConfig$), - map((data) => (data == null ? null : new ServerConfig(data))), - ) - .subscribe((config) => this._serverConfig.next(config)); - - this.inited = true; - } - - getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { - return this.serverConfig$.pipe( - map((serverConfig) => { - if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { - return defaultValue; - } - - return serverConfig.featureStates[key] as T; - }), - ); - } - - async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { - return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); - } - - triggerServerConfigFetch() { - this._forceFetchConfig.next(); - } - - private async saveConfig(data: ServerConfigData) { - if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) { - return; - } - - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - await this.stateService.setServerConfig(data); - await this.environmentService.setCloudRegion(userId, data.environment?.cloudRegion); - } - - /** - * Verifies whether the server version meets the minimum required version - * @param minimumRequiredServerVersion The minimum version required - * @returns True if the server version is greater than or equal to the minimum required version - */ - checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { - return this.serverConfig$.pipe( - map((serverConfig) => { - if (serverConfig == null) { - return false; - } - const serverVersion = new SemVer(serverConfig.version); - return serverVersion.compare(minimumRequiredServerVersion) >= 0; - }), - ); - } -} diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts new file mode 100644 index 000000000000..9532b903d372 --- /dev/null +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -0,0 +1,177 @@ +import { + NEVER, + Observable, + Subject, + combineLatest, + firstValueFrom, + map, + mergeWith, + of, + shareReplay, + switchMap, + tap, +} from "rxjs"; +import { SemVer } from "semver"; + +import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; +import { UserId } from "../../../types/guid"; +import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; +import { ConfigService } from "../../abstractions/config/config.service"; +import { ServerConfig } from "../../abstractions/config/server-config"; +import { EnvironmentService, Region } from "../../abstractions/environment.service"; +import { LogService } from "../../abstractions/log.service"; +import { ServerConfigData } from "../../models/data/server-config.data"; +import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state"; + +export const RETRIEVAL_INTERVAL = 3_600_000; // 1 hour + +export type ApiUrl = string; + +export const USER_SERVER_CONFIG = new UserKeyDefinition(CONFIG_DISK, "serverConfig", { + deserializer: (data) => (data == null ? null : ServerConfig.fromJSON(data)), + clearOn: ["logout"], +}); + +// TODO MDG: When to clean these up? +export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record( + CONFIG_DISK, + "byServer", + { + deserializer: (data) => (data == null ? null : ServerConfig.fromJSON(data)), + }, +); + +// FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it. +export class DefaultConfigService implements ConfigService { + private failedFetchFallbackSubject = new Subject(); + + serverConfig$: Observable; + + cloudRegion$: Observable; + + constructor( + private configApiService: ConfigApiServiceAbstraction, + private environmentService: EnvironmentService, + private logService: LogService, + private stateProvider: StateProvider, + ) { + const apiUrl$ = this.environmentService.environment$.pipe( + map((environment) => environment.getApiUrl()), + ); + + this.serverConfig$ = combineLatest([this.stateProvider.activeUserId$, apiUrl$]).pipe( + switchMap(([userId, apiUrl]) => { + const config$ = + userId == null ? this.globalConfigFor$(apiUrl) : this.userConfigFor$(userId); + return config$.pipe(map((config) => [config, userId, apiUrl] as const)); + }), + tap(async (rec) => { + const [existingConfig, userId, apiUrl] = rec; + // Grab new config if older retrieval interval + if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) { + await this.renewConfig(existingConfig, userId, apiUrl); + } + }), + switchMap(([existingConfig]) => { + // If we needed to fetch, stop this emit, we'll get a new one after update + // This is split up with the above tap because we need to return an observable from a failed promise, + // which isn't very doable since promises are converted to observables in switchMap + if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) { + return NEVER; + } + return of(existingConfig); + }), + // If fetch fails, we'll emit on this subject to fallback to the existing config + mergeWith(this.failedFetchFallbackSubject), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + this.cloudRegion$ = this.serverConfig$.pipe( + map((config) => config?.environment?.cloudRegion ?? Region.US), + ); + } + getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { + return this.serverConfig$.pipe( + map((serverConfig) => { + if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { + return defaultValue; + } + + return serverConfig.featureStates[key] as T; + }), + ); + } + + async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { + return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); + } + + checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { + return this.serverConfig$.pipe( + map((serverConfig) => { + if (serverConfig == null) { + return false; + } + const serverVersion = new SemVer(serverConfig.version); + return serverVersion.compare(minimumRequiredServerVersion) >= 0; + }), + ); + } + + async ensureConfigFetched() { + // Triggering a retrieval for the given user ensures that the config is less than RETRIEVAL_INTERVAL old + await firstValueFrom(this.serverConfig$); + } + + private olderThanRetrievalInterval(date: Date) { + return new Date().getTime() - date.getTime() > RETRIEVAL_INTERVAL; + } + + // Updates the on-disk configuration with a newly retrieved configuration + private async renewConfig( + existingConfig: ServerConfig, + userId: UserId, + apiUrl: string, + ): Promise { + try { + const response = await this.configApiService.get(userId); + const newConfig = new ServerConfig(new ServerConfigData(response)); + + // Update the environment region + if ( + newConfig?.environment?.cloudRegion != null && + existingConfig?.environment?.cloudRegion != newConfig.environment.cloudRegion + ) { + // Null userId sets global, otherwise sets to the given user + await this.environmentService.setCloudRegion(userId, newConfig?.environment?.cloudRegion); + } + + if (userId == null) { + // update global state with new pulled config + await this.stateProvider.getGlobal(GLOBAL_SERVER_CONFIGURATIONS).update((configs) => { + return { ...configs, [apiUrl]: newConfig }; + }); + } else { + // update state with new pulled config + await this.stateProvider.setUserState(USER_SERVER_CONFIG, newConfig, userId); + } + } catch (e) { + // mutate error to be handled by catchError + this.logService.error( + `Unable to fetch ServerConfig from ${apiUrl}: ${(e as Error)?.message}`, + ); + // Emit the existing config + this.failedFetchFallbackSubject.next(existingConfig); + } + } + + private globalConfigFor$(apiUrl: string): Observable { + return this.stateProvider + .getGlobal(GLOBAL_SERVER_CONFIGURATIONS) + .state$.pipe(map((configs) => configs?.[apiUrl])); + } + + private userConfigFor$(userId: UserId): Observable { + return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$; + } +} diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 56fb91dd52bf..bbcc00e5629e 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -32,7 +32,6 @@ import { import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; -import { ServerConfigData } from "../models/data/server-config.data"; import { Account, AccountData, AccountSettings } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { GlobalState } from "../models/domain/global-state"; @@ -1377,23 +1376,6 @@ export class StateService< ); } - async setServerConfig(value: ServerConfigData, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.serverConfig = value; - return await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getServerConfig(options: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.serverConfig; - } - async getDeepLinkRedirectUrl(options?: StorageOptions): Promise { return ( await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index bf0d162eeec6..b44c449c217a 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -73,6 +73,9 @@ export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", }); export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk"); export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk"); +export const CONFIG_DISK = new StateDefinition("config", "disk", { + web: "disk-local", +}); export const CRYPTO_DISK = new StateDefinition("crypto", "disk"); export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory"); export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 1b057fda4d07..b932a7186eef 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -44,6 +44,7 @@ import { MergeEnvironmentState } from "./migrations/45-merge-environment-state"; import { DeleteBiometricPromptCancelledData } from "./migrations/46-delete-orphaned-biometric-prompt-data"; import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settings"; import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-state-provider"; +import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -52,8 +53,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 48; - +export const CURRENT_VERSION = 49; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -103,7 +103,8 @@ export function createMigrationBuilder() { .with(MergeEnvironmentState, 44, 45) .with(DeleteBiometricPromptCancelledData, 45, 46) .with(MoveDesktopSettingsMigrator, 46, 47) - .with(MoveDdgToStateProviderMigrator, 47, CURRENT_VERSION); + .with(MoveDdgToStateProviderMigrator, 47, 48) + .with(AccountServerConfigMigrator, 48, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts new file mode 100644 index 000000000000..4533a754b6ad --- /dev/null +++ b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts @@ -0,0 +1,112 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { AccountServerConfigMigrator } from "./49-move-account-server-configs"; + +describe("AccountServerConfigMigrator", () => { + const migrator = new AccountServerConfigMigrator(48, 49); + + describe("all data", () => { + function toMigrate() { + return { + authenticatedAccounts: ["user1", "user2"], + user1: { + settings: { + serverConfig: { + config: "user1 server config", + }, + }, + }, + user2: { + settings: { + serverConfig: { + config: "user2 server config", + }, + }, + }, + }; + } + + function migrated() { + return { + authenticatedAccounts: ["user1", "user2"], + + user1: { + settings: {}, + }, + user2: { + settings: {}, + }, + user_user1_config_serverConfig: { + config: "user1 server config", + }, + user_user2_config_serverConfig: { + config: "user2 server config", + }, + }; + } + + function rolledBack(previous: object) { + return { + ...previous, + user_user1_config_serverConfig: null as unknown, + user_user2_config_serverConfig: null as unknown, + }; + } + + it("migrates", async () => { + const output = await runMigrator(migrator, toMigrate(), "migrate"); + expect(output).toEqual(migrated()); + }); + + it("rolls back", async () => { + const output = await runMigrator(migrator, migrated(), "rollback"); + expect(output).toEqual(rolledBack(toMigrate())); + }); + }); + + describe("missing parts", () => { + function toMigrate() { + return { + authenticatedAccounts: ["user1", "user2"], + user1: { + settings: { + serverConfig: { + config: "user1 server config", + }, + }, + }, + user2: null as unknown, + }; + } + + function migrated() { + return { + authenticatedAccounts: ["user1", "user2"], + user1: { + settings: {}, + }, + user2: null as unknown, + user_user1_config_serverConfig: { + config: "user1 server config", + }, + }; + } + + function rollback(previous: object) { + return { + ...previous, + user_user1_config_serverConfig: null as unknown, + }; + } + + it("migrates", async () => { + const output = await runMigrator(migrator, toMigrate(), "migrate"); + expect(output).toEqual(migrated()); + }); + + it("rolls back", async () => { + const output = await runMigrator(migrator, migrated(), "rollback"); + expect(output).toEqual(rollback(toMigrate())); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts new file mode 100644 index 000000000000..8cc25a322dd0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts @@ -0,0 +1,51 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +const CONFIG_DISK: StateDefinitionLike = { name: "config" }; +export const USER_SERVER_CONFIG: KeyDefinitionLike = { + stateDefinition: CONFIG_DISK, + key: "serverConfig", +}; + +// Note: no need to migrate global configs, they don't currently exist + +type ExpectedAccountType = { + settings?: { + serverConfig?: unknown; + }; +}; + +export class AccountServerConfigMigrator extends Migrator<48, 49> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + if (account?.settings?.serverConfig != null) { + await helper.setToUser(userId, USER_SERVER_CONFIG, account.settings.serverConfig); + delete account.settings.serverConfig; + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const serverConfig = await helper.getFromUser(userId, USER_SERVER_CONFIG); + + if (serverConfig) { + account ??= {}; + account.settings ??= {}; + + account.settings.serverConfig = serverConfig; + await helper.setToUser(userId, USER_SERVER_CONFIG, null); + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index bcd4bb983628..c37472478164 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -7,7 +7,7 @@ import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; -import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -108,7 +108,7 @@ describe("Cipher Service", () => { const i18nService = mock(); const searchService = mock(); const encryptService = mock(); - const configService = mock(); + const configService = mock(); let cipherService: CipherService; let cipherObj: Cipher; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 829ee5ed4ee6..4a6e96ead769 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -9,7 +9,7 @@ import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; -import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -72,7 +72,7 @@ export class CipherService implements CipherServiceAbstraction { private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} async getDecryptedCipherCache(): Promise { diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts index 2f76c5043afe..9757e24d8fbb 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts @@ -4,7 +4,7 @@ import { of } from "rxjs"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; -import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../../platform/abstractions/config/config.service"; import { Utils } from "../../../platform/misc/utils"; import { Fido2AuthenticatorError, @@ -30,7 +30,7 @@ const VaultUrl = "https://vault.bitwarden.com"; describe("FidoAuthenticatorService", () => { let authenticator!: MockProxy; - let configService!: MockProxy; + let configService!: MockProxy; let authService!: MockProxy; let vaultSettingsService: MockProxy; let domainSettingsService: MockProxy; @@ -39,7 +39,7 @@ describe("FidoAuthenticatorService", () => { beforeEach(async () => { authenticator = mock(); - configService = mock(); + configService = mock(); authService = mock(); vaultSettingsService = mock(); domainSettingsService = mock(); diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.ts b/libs/common/src/vault/services/fido2/fido2-client.service.ts index c725b2263721..bfc8cbe915ad 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -4,7 +4,7 @@ import { parse } from "tldts"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; -import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../../platform/abstractions/config/config.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { Utils } from "../../../platform/misc/utils"; import { @@ -40,7 +40,7 @@ import { Fido2Utils } from "./fido2-utils"; export class Fido2ClientService implements Fido2ClientServiceAbstraction { constructor( private authenticator: Fido2AuthenticatorService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private authService: AuthService, private vaultSettingsService: VaultSettingsService, private domainSettingsService: DomainSettingsService, diff --git a/libs/common/test.setup.ts b/libs/common/test.setup.ts index c50c7ca227e4..d857751b51bb 100644 --- a/libs/common/test.setup.ts +++ b/libs/common/test.setup.ts @@ -1,6 +1,7 @@ import { webcrypto } from "crypto"; import { toEqualBuffer } from "./spec"; +import { toAlmostEqual } from "./spec/matchers/to-almost-equal"; Object.defineProperty(window, "crypto", { value: webcrypto, @@ -10,8 +11,15 @@ Object.defineProperty(window, "crypto", { expect.extend({ toEqualBuffer: toEqualBuffer, + toAlmostEqual: toAlmostEqual, }); export interface CustomMatchers { toEqualBuffer(expected: Uint8Array | ArrayBuffer): R; + /** + * Matches the expected date within an optional ms precision + * @param expected The expected date + * @param msPrecision The optional precision in milliseconds + */ + toAlmostEqual(expected: Date, msPrecision?: number): R; } From 5de217717534494487bc748a85e67743dfddc6f2 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 27 Mar 2024 13:27:44 -0400 Subject: [PATCH 26/51] only initialize user decryption options if present on response obj (#8508) --- .../models/domain/user-decryption-options.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/libs/auth/src/common/models/domain/user-decryption-options.ts b/libs/auth/src/common/models/domain/user-decryption-options.ts index c600c8be476f..ca4046f36e8c 100644 --- a/libs/auth/src/common/models/domain/user-decryption-options.ts +++ b/libs/auth/src/common/models/domain/user-decryption-options.ts @@ -15,11 +15,14 @@ export class KeyConnectorUserDecryptionOption { /** * Initializes a new instance of the KeyConnectorUserDecryptionOption from a response object. * @param response The key connector user decryption option response object. - * @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the response is nullish. + * @returns A new instance of the KeyConnectorUserDecryptionOption or undefined if `response` is nullish. */ static fromResponse( response: KeyConnectorUserDecryptionOptionResponse, - ): KeyConnectorUserDecryptionOption { + ): KeyConnectorUserDecryptionOption | undefined { + if (response == null) { + return undefined; + } const options = new KeyConnectorUserDecryptionOption(); options.keyConnectorUrl = response?.keyConnectorUrl ?? null; return options; @@ -28,11 +31,14 @@ export class KeyConnectorUserDecryptionOption { /** * Initializes a new instance of a KeyConnectorUserDecryptionOption from a JSON object. * @param obj JSON object to deserialize. - * @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the JSON object is nullish. + * @returns A new instance of the KeyConnectorUserDecryptionOption or undefined if `obj` is nullish. */ static fromJSON( obj: Jsonify, - ): KeyConnectorUserDecryptionOption { + ): KeyConnectorUserDecryptionOption | undefined { + if (obj == null) { + return undefined; + } return Object.assign(new KeyConnectorUserDecryptionOption(), obj); } } @@ -52,11 +58,14 @@ export class TrustedDeviceUserDecryptionOption { /** * Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object. * @param response The trusted device user decryption option response object. - * @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the response is nullish. + * @returns A new instance of the TrustedDeviceUserDecryptionOption or undefined if `response` is nullish. */ static fromResponse( response: TrustedDeviceUserDecryptionOptionResponse, - ): TrustedDeviceUserDecryptionOption { + ): TrustedDeviceUserDecryptionOption | undefined { + if (response == null) { + return undefined; + } const options = new TrustedDeviceUserDecryptionOption(); options.hasAdminApproval = response?.hasAdminApproval ?? false; options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false; @@ -67,11 +76,14 @@ export class TrustedDeviceUserDecryptionOption { /** * Initializes a new instance of the TrustedDeviceUserDecryptionOption from a JSON object. * @param obj JSON object to deserialize. - * @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the JSON object is nullish. + * @returns A new instance of the TrustedDeviceUserDecryptionOption or undefined if `obj` is nullish. */ static fromJSON( obj: Jsonify, - ): TrustedDeviceUserDecryptionOption { + ): TrustedDeviceUserDecryptionOption | undefined { + if (obj == null) { + return undefined; + } return Object.assign(new TrustedDeviceUserDecryptionOption(), obj); } } From aaa745ec3681393e0dd93c5c52c1d6bab1ae30a5 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 27 Mar 2024 17:17:17 -0400 Subject: [PATCH 27/51] Return correct master password hash from login strategies (#8518) --- .../password-login.strategy.ts | 24 +++++++++++-------- .../login-strategy.service.ts | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index be93d39ebc46..427f8178e47f 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -63,14 +63,12 @@ export class PasswordLoginStrategyData implements LoginStrategyData { } export class PasswordLoginStrategy extends LoginStrategy { - /** - * The email address of the user attempting to log in. - */ + /** The email address of the user attempting to log in. */ email$: Observable; - /** - * The master key hash of the user attempting to log in. - */ - masterKeyHash$: Observable; + /** The master key hash used for authentication */ + serverMasterKeyHash$: Observable; + /** The local master key hash we store client side */ + localMasterKeyHash$: Observable; protected cache: BehaviorSubject; @@ -107,7 +105,10 @@ export class PasswordLoginStrategy extends LoginStrategy { this.cache = new BehaviorSubject(data); this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email)); - this.masterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash)); + this.serverMasterKeyHash$ = this.cache.pipe( + map((state) => state.tokenRequest.masterPasswordHash), + ); + this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash)); } override async logIn(credentials: PasswordLoginCredentials) { @@ -123,11 +124,14 @@ export class PasswordLoginStrategy extends LoginStrategy { data.masterKey, HashPurpose.LocalAuthorization, ); - const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, data.masterKey); + const serverMasterKeyHash = await this.cryptoService.hashMasterKey( + masterPassword, + data.masterKey, + ); data.tokenRequest = new PasswordTokenRequest( email, - masterKeyHash, + serverMasterKeyHash, captchaToken, await this.buildTwoFactor(twoFactor, email), await this.buildDeviceRequest(), diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 5dbc3397cf48..428258308aa2 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -137,8 +137,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getMasterPasswordHash(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("masterKeyHash$" in strategy) { - return await firstValueFrom(strategy.masterKeyHash$); + if ("serverMasterKeyHash$" in strategy) { + return await firstValueFrom(strategy.serverMasterKeyHash$); } return null; } From d9bec7f9846c8b2be8ac02bef8f8521733a46ba6 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 27 Mar 2024 17:22:56 -0400 Subject: [PATCH 28/51] send captcha bypass token on 2fa token request (#8511) --- .../password-login.strategy.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 427f8178e47f..d3de3ea6bac5 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -37,15 +37,11 @@ export class PasswordLoginStrategyData implements LoginStrategyData { /** User's entered email obtained pre-login. Always present in MP login. */ userEnteredEmail: string; - + /** If 2fa is required, token is returned to bypass captcha */ captchaBypassToken?: string; - /** - * The local version of the user's master key hash - */ + /** The local version of the user's master key hash */ localMasterKeyHash: string; - /** - * The user's master key - */ + /** The user's master key */ masterKey: MasterKey; /** * Tracks if the user needs to update their password due to @@ -175,10 +171,10 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactor: TokenTwoFactorRequest, captchaResponse: string, ): Promise { - this.cache.next({ - ...this.cache.value, - captchaBypassToken: captchaResponse ?? this.cache.value.captchaBypassToken, - }); + const data = this.cache.value; + data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken; + this.cache.next(data); + const result = await super.logInTwoFactor(twoFactor); // 2FA was successful, save the force update password options with the state service if defined From 8cdc94076e9cccda9199bf48a4b7e2fb4eabc1b7 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:46:56 -0400 Subject: [PATCH 29/51] Auth/PM-7092 - Fix CLI login via API key not working due to TokenService changes (#8499) * PM-7092 - Fix CLI login via API key not working (it apparently receives an undefined refresh token which was rejected by setTokens) * PM-7092 - Fix base login strategy tests * PM-7092 - per discucssion with jake, refactor setTokens to accept optional refresh token instead of exposing setRefreshToken as public. --- .../login-strategies/login.strategy.spec.ts | 2 +- .../common/login-strategies/login.strategy.ts | 2 +- .../src/auth/abstractions/token.service.ts | 7 +++--- .../src/auth/services/token.service.spec.ts | 24 ++++++++----------- .../common/src/auth/services/token.service.ts | 12 ++++++---- libs/common/src/services/api.service.ts | 2 +- .../vault-timeout-settings.service.ts | 2 +- 7 files changed, 26 insertions(+), 25 deletions(-) diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index ed40797df517..42541808c8ca 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -186,9 +186,9 @@ describe("LoginStrategy", () => { expect(tokenService.setTokens).toHaveBeenCalledWith( accessToken, - refreshToken, mockVaultTimeoutAction, mockVaultTimeout, + refreshToken, ); expect(stateService.addAccount).toHaveBeenCalledWith( diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index eef5626493b6..8e927c2cc485 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -182,9 +182,9 @@ export abstract class LoginStrategy { // User id will be derived from the access token. await this.tokenService.setTokens( tokenResponse.accessToken, - tokenResponse.refreshToken, vaultTimeoutAction as VaultTimeoutAction, vaultTimeout, + tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. ); await this.stateService.addAccount( diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index d2358314d79e..18366c5f1b32 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -10,17 +10,18 @@ export abstract class TokenService { * Note 2: this method also enforces always setting the access token and the refresh token together as * we can retrieve the user id required to set the refresh token from the access token for efficiency. * @param accessToken The access token to set. - * @param refreshToken The refresh token to set. - * @param clientIdClientSecret The API Key Client ID and Client Secret to set. * @param vaultTimeoutAction The action to take when the vault times out. * @param vaultTimeout The timeout for the vault. + * @param refreshToken The optional refresh token to set. Note: this is undefined when using the CLI Login Via API Key flow + * @param clientIdClientSecret The API Key Client ID and Client Secret to set. + * * @returns A promise that resolves when the tokens have been set. */ setTokens: ( accessToken: string, - refreshToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, + refreshToken?: string, clientIdClientSecret?: [string, string], ) => Promise; diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 63c581910a86..8e8ed0885322 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -991,6 +991,7 @@ describe("TokenService", () => { refreshToken, VaultTimeoutAction.Lock, null, + null, ); // Assert await expect(result).rejects.toThrow("User id not found. Cannot save refresh token."); @@ -1854,7 +1855,7 @@ describe("TokenService", () => { // Act // Note: passing a valid access token so that a valid user id can be determined from the access token - await tokenService.setTokens(accessTokenJwt, refreshToken, vaultTimeoutAction, vaultTimeout, [ + await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken, [ clientId, clientSecret, ]); @@ -1901,7 +1902,7 @@ describe("TokenService", () => { tokenService.setClientSecret = jest.fn(); // Act - await tokenService.setTokens(accessTokenJwt, refreshToken, vaultTimeoutAction, vaultTimeout); + await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken); // Assert expect((tokenService as any)._setAccessToken).toHaveBeenCalledWith( @@ -1933,9 +1934,9 @@ describe("TokenService", () => { // Act const result = tokenService.setTokens( accessToken, - refreshToken, vaultTimeoutAction, vaultTimeout, + refreshToken, ); // Assert @@ -1952,32 +1953,27 @@ describe("TokenService", () => { // Act const result = tokenService.setTokens( accessToken, - refreshToken, vaultTimeoutAction, vaultTimeout, + refreshToken, ); // Assert - await expect(result).rejects.toThrow("Access token and refresh token are required."); + await expect(result).rejects.toThrow("Access token is required."); }); - it("should throw an error if the refresh token is missing", async () => { + it("should not throw an error if the refresh token is missing and it should just not set it", async () => { // Arrange - const accessToken = "accessToken"; const refreshToken: string = null; const vaultTimeoutAction = VaultTimeoutAction.Lock; const vaultTimeout = 30; + (tokenService as any).setRefreshToken = jest.fn(); // Act - const result = tokenService.setTokens( - accessToken, - refreshToken, - vaultTimeoutAction, - vaultTimeout, - ); + await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken); // Assert - await expect(result).rejects.toThrow("Access token and refresh token are required."); + expect((tokenService as any).setRefreshToken).not.toHaveBeenCalled(); }); }); diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index a1dc7ecf21e2..dd011eb40bc8 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -149,13 +149,13 @@ export class TokenService implements TokenServiceAbstraction { async setTokens( accessToken: string, - refreshToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, + refreshToken?: string, clientIdClientSecret?: [string, string], ): Promise { - if (!accessToken || !refreshToken) { - throw new Error("Access token and refresh token are required."); + if (!accessToken) { + throw new Error("Access token is required."); } // get user id the access token @@ -166,7 +166,11 @@ export class TokenService implements TokenServiceAbstraction { } await this._setAccessToken(accessToken, vaultTimeoutAction, vaultTimeout, userId); - await this.setRefreshToken(refreshToken, vaultTimeoutAction, vaultTimeout, userId); + + if (refreshToken) { + await this.setRefreshToken(refreshToken, vaultTimeoutAction, vaultTimeout, userId); + } + if (clientIdClientSecret != null) { await this.setClientId(clientIdClientSecret[0], vaultTimeoutAction, vaultTimeout, userId); await this.setClientSecret(clientIdClientSecret[1], vaultTimeoutAction, vaultTimeout, userId); diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index b6c2ab5c2230..6306eb1e2883 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1780,9 +1780,9 @@ export class ApiService implements ApiServiceAbstraction { await this.tokenService.setTokens( tokenResponse.accessToken, - tokenResponse.refreshToken, vaultTimeoutAction as VaultTimeoutAction, vaultTimeout, + tokenResponse.refreshToken, ); } else { const error = await this.handleError(response, true, true); diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index 4eb9e7769927..a8afc632972a 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -52,7 +52,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA await this.stateService.setVaultTimeoutAction(action); - await this.tokenService.setTokens(accessToken, refreshToken, action, timeout, [ + await this.tokenService.setTokens(accessToken, action, timeout, refreshToken, [ clientId, clientSecret, ]); From 5cb2e99b2fd3899a3a940fb985ed08d2d9f88178 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:08:28 +1000 Subject: [PATCH 30/51] [AC-1724] Remove BulkCollectionAccess feature flag (#8502) --- .../vault/components/vault-items/vault-items.component.ts | 2 +- apps/web/src/app/vault/individual-vault/vault.component.html | 1 - apps/web/src/app/vault/individual-vault/vault.component.ts | 5 ----- apps/web/src/app/vault/org-vault/vault.component.html | 4 +--- apps/web/src/app/vault/org-vault/vault.component.ts | 4 ---- libs/common/src/enums/feature-flag.enum.ts | 1 - 6 files changed, 2 insertions(+), 15 deletions(-) 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 b17eed8ca113..7a8e858ba576 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 @@ -37,7 +37,7 @@ export class VaultItemsComponent { @Input() showBulkMove: boolean; @Input() showBulkTrashOptions: boolean; // Encompasses functionality only available from the organization vault context - @Input() showAdminActions: boolean; + @Input() showAdminActions = false; @Input() allOrganizations: Organization[] = []; @Input() allCollections: CollectionView[] = []; @Input() allGroups: GroupView[] = []; diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index b59e554f5ac7..5f90f8d440bc 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -50,7 +50,6 @@ [cloneableOrganizationCiphers]="false" [showAdminActions]="false" (onEvent)="onVaultItemsEvent($event)" - [showBulkEditCollectionAccess]="showBulkCollectionAccess$ | async" >

| undefined; protected canCreateCollections = false; protected currentSearchText$: Observable; - protected showBulkCollectionAccess$ = this.configService.getFeatureFlag$( - FeatureFlag.BulkCollectionAccess, - false, - ); private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 242a03b99554..4bec92b5db3f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -51,9 +51,7 @@ [cloneableOrganizationCiphers]="true" [showAdminActions]="true" (onEvent)="onVaultItemsEvent($event)" - [showBulkEditCollectionAccess]=" - (showBulkEditCollectionAccess$ | async) && organization?.flexibleCollections - " + [showBulkEditCollectionAccess]="organization?.flexibleCollections" [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" > 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 028198723bf3..a267612bd627 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -143,10 +143,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected currentSearchText$: Observable; protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; - protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$( - FeatureFlag.BulkCollectionAccess, - false, - ); private _flexibleCollectionsV1FlagEnabled: boolean; protected get flexibleCollectionsV1Enabled(): boolean { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 8a5075e96f34..ca5ccc17b532 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -2,7 +2,6 @@ export enum FeatureFlag { BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional - BulkCollectionAccess = "bulk-collection-access", VaultOnboarding = "vault-onboarding", GeneratorToolsModernization = "generator-tools-modernization", KeyRotationImprovements = "key-rotation-improvements", From b3b344866e8276a50a0ba5b2be07939f3b60716e Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:28:51 +1000 Subject: [PATCH 31/51] [AC-2278] [AC-2296] Use SafeProvider in browser services module (#8418) --- .../popup/services/unauth-guard.service.ts | 3 - .../popup/services/popup-search.service.ts | 6 +- .../src/popup/services/services.module.ts | 661 +++++++++--------- .../src/platform/utils/safe-provider.ts | 67 +- .../src/services/jslib-services.module.ts | 5 +- 5 files changed, 381 insertions(+), 361 deletions(-) diff --git a/apps/browser/src/auth/popup/services/unauth-guard.service.ts b/apps/browser/src/auth/popup/services/unauth-guard.service.ts index 062239a7d36a..0fbb4ac9baee 100644 --- a/apps/browser/src/auth/popup/services/unauth-guard.service.ts +++ b/apps/browser/src/auth/popup/services/unauth-guard.service.ts @@ -1,8 +1,5 @@ -import { Injectable } from "@angular/core"; - import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; -@Injectable() export class UnauthGuardService extends BaseUnauthGuardService { protected homepage = "tabs/current"; } diff --git a/apps/browser/src/popup/services/popup-search.service.ts b/apps/browser/src/popup/services/popup-search.service.ts index 7eea1265a236..bc5e565e6cad 100644 --- a/apps/browser/src/popup/services/popup-search.service.ts +++ b/apps/browser/src/popup/services/popup-search.service.ts @@ -1,14 +1,14 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SearchService } from "@bitwarden/common/services/search.service"; export class PopupSearchService extends SearchService { constructor( private mainSearchService: SearchService, - consoleLogService: ConsoleLogService, + logService: LogService, i18nService: I18nService, ) { - super(consoleLogService, i18nService); + super(logService, i18nService); } clearIndex() { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7ab04603e45e..33593b56dd40 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,15 +1,18 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; +import { Router } from "@angular/router"; import { ToastrService } from "ngx-toastr"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; +import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { MEMORY_STORAGE, SECURE_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, + SafeInjectionToken, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { @@ -129,323 +132,351 @@ function getBgService(service: keyof MainBackground) { }; } +/** + * Provider definitions used in the ngModule. + * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. + * If you need help please ask for it, do NOT change the type of this array. + */ +const safeProviders: SafeProvider[] = [ + safeProvider(InitService), + safeProvider(DebounceNavigationService), + safeProvider(DialogService), + safeProvider(PopupCloseWarningService), + safeProvider({ + provide: APP_INITIALIZER as SafeInjectionToken<() => Promise>, + useFactory: (initService: InitService) => initService.init(), + deps: [InitService], + multi: true, + }), + safeProvider({ + provide: BaseUnauthGuardService, + useClass: UnauthGuardService, + deps: [AuthServiceAbstraction, Router], + }), + safeProvider({ + provide: MessagingService, + useFactory: () => { + return needsBackgroundInit + ? new BrowserMessagingPrivateModePopupService() + : new BrowserMessagingService(); + }, + deps: [], + }), + safeProvider({ + provide: TwoFactorService, + useFactory: getBgService("twoFactorService"), + deps: [], + }), + safeProvider({ + provide: AuthServiceAbstraction, + useFactory: getBgService("authService"), + deps: [], + }), + safeProvider({ + provide: LoginStrategyServiceAbstraction, + useFactory: getBgService("loginStrategyService"), + deps: [], + }), + safeProvider({ + provide: SsoLoginServiceAbstraction, + useFactory: getBgService("ssoLoginService"), + deps: [], + }), + safeProvider({ + provide: SearchServiceAbstraction, + useFactory: (logService: LogService, i18nService: I18nServiceAbstraction) => { + return new PopupSearchService( + getBgService("searchService")(), + logService, + i18nService, + ); + }, + deps: [LogService, I18nServiceAbstraction], + }), + safeProvider({ + provide: CipherFileUploadService, + useFactory: getBgService("cipherFileUploadService"), + deps: [], + }), + safeProvider({ + provide: CipherService, + useFactory: getBgService("cipherService"), + deps: [], + }), + safeProvider({ + provide: CryptoFunctionService, + useFactory: () => new WebCryptoFunctionService(window), + deps: [], + }), + safeProvider({ + provide: CollectionService, + useFactory: getBgService("collectionService"), + deps: [], + }), + safeProvider({ + provide: LogService, + useFactory: (platformUtilsService: PlatformUtilsService) => + new ConsoleLogService(platformUtilsService.isDev()), + deps: [PlatformUtilsService], + }), + safeProvider({ + provide: EnvironmentService, + useExisting: BrowserEnvironmentService, + }), + safeProvider({ + provide: BrowserEnvironmentService, + useClass: BrowserEnvironmentService, + deps: [LogService, StateProvider, AccountServiceAbstraction], + }), + safeProvider({ + provide: TotpService, + useFactory: getBgService("totpService"), + deps: [], + }), + safeProvider({ + provide: I18nServiceAbstraction, + useFactory: (globalStateProvider: GlobalStateProvider) => { + return new I18nService(BrowserApi.getUILanguage(), globalStateProvider); + }, + deps: [GlobalStateProvider], + }), + safeProvider({ + provide: CryptoService, + useFactory: (encryptService: EncryptService) => { + const cryptoService = getBgService("cryptoService")(); + new ContainerService(cryptoService, encryptService).attachToGlobal(self); + return cryptoService; + }, + deps: [EncryptService], + }), + safeProvider({ + provide: AuthRequestServiceAbstraction, + useFactory: getBgService("authRequestService"), + deps: [], + }), + safeProvider({ + provide: DeviceTrustCryptoServiceAbstraction, + useFactory: getBgService("deviceTrustCryptoService"), + deps: [], + }), + safeProvider({ + provide: DevicesServiceAbstraction, + useFactory: getBgService("devicesService"), + deps: [], + }), + safeProvider({ + provide: PlatformUtilsService, + useExisting: ForegroundPlatformUtilsService, + }), + safeProvider({ + provide: ForegroundPlatformUtilsService, + useClass: ForegroundPlatformUtilsService, + useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + return new ForegroundPlatformUtilsService( + sanitizer, + toastrService, + (clipboardValue: string, clearMs: number) => { + void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); + }, + async () => { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlock"); + if (!response.result) { + throw response.error; + } + return response.result; + }, + window, + ); + }, + deps: [DomSanitizer, ToastrService], + }), + safeProvider({ + provide: PasswordGenerationServiceAbstraction, + useFactory: getBgService("passwordGenerationService"), + deps: [], + }), + safeProvider({ + provide: SyncService, + useFactory: getBgService("syncService"), + deps: [], + }), + safeProvider({ + provide: DomainSettingsService, + useClass: DefaultDomainSettingsService, + deps: [StateProvider], + }), + safeProvider({ + provide: AbstractStorageService, + useClass: BrowserLocalStorageService, + deps: [], + }), + safeProvider({ + provide: AutofillService, + useFactory: getBgService("autofillService"), + deps: [], + }), + safeProvider({ + provide: VaultExportServiceAbstraction, + useFactory: getBgService("exportService"), + deps: [], + }), + safeProvider({ + provide: KeyConnectorService, + useFactory: getBgService("keyConnectorService"), + deps: [], + }), + safeProvider({ + provide: UserVerificationService, + useFactory: getBgService("userVerificationService"), + deps: [], + }), + safeProvider({ + provide: VaultTimeoutSettingsService, + useFactory: getBgService("vaultTimeoutSettingsService"), + deps: [], + }), + safeProvider({ + provide: VaultTimeoutService, + useFactory: getBgService("vaultTimeoutService"), + deps: [], + }), + safeProvider({ + provide: NotificationsService, + useFactory: getBgService("notificationsService"), + deps: [], + }), + safeProvider({ + provide: VaultFilterService, + useClass: VaultFilterService, + deps: [ + OrganizationService, + FolderServiceAbstraction, + CipherService, + CollectionService, + PolicyService, + StateProvider, + AccountServiceAbstraction, + ], + }), + safeProvider({ + provide: SECURE_STORAGE, + useExisting: AbstractStorageService, // Secure storage is not available in the browser, so we use normal storage instead and warn users when it is used. + }), + safeProvider({ + provide: MEMORY_STORAGE, + useFactory: getBgService("memoryStorageService"), + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_MEMORY_STORAGE, + useClass: ForegroundMemoryStorageService, + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_DISK_STORAGE, + useExisting: AbstractStorageService, + }), + safeProvider({ + provide: StateServiceAbstraction, + useFactory: ( + storageService: AbstractStorageService, + secureStorageService: AbstractStorageService, + memoryStorageService: AbstractMemoryStorageService, + logService: LogService, + accountService: AccountServiceAbstraction, + environmentService: EnvironmentService, + tokenService: TokenService, + migrationRunner: MigrationRunner, + ) => { + return new BrowserStateService( + storageService, + secureStorageService, + memoryStorageService, + logService, + new StateFactory(GlobalState, Account), + accountService, + environmentService, + tokenService, + migrationRunner, + ); + }, + deps: [ + AbstractStorageService, + SECURE_STORAGE, + MEMORY_STORAGE, + LogService, + AccountServiceAbstraction, + EnvironmentService, + TokenService, + MigrationRunner, + ], + }), + safeProvider({ + provide: UsernameGenerationServiceAbstraction, + useFactory: getBgService("usernameGenerationService"), + deps: [], + }), + safeProvider({ + provide: BaseStateServiceAbstraction, + useExisting: StateServiceAbstraction, + deps: [], + }), + safeProvider({ + provide: FileDownloadService, + useClass: BrowserFileDownloadService, + deps: [], + }), + safeProvider({ + provide: LoginServiceAbstraction, + useClass: LoginService, + deps: [StateServiceAbstraction], + }), + safeProvider({ + provide: SYSTEM_THEME_OBSERVABLE, + useFactory: (platformUtilsService: PlatformUtilsService) => { + // Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light. + // In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed. + let windowContext = window; + const backgroundWindow = BrowserApi.getBackgroundPage(); + if (platformUtilsService.isSafari() && backgroundWindow) { + windowContext = backgroundWindow; + } + + return AngularThemingService.createSystemThemeFromWindow(windowContext); + }, + deps: [PlatformUtilsService], + }), + safeProvider({ + provide: FilePopoutUtilsService, + useFactory: (platformUtilsService: PlatformUtilsService) => { + return new FilePopoutUtilsService(platformUtilsService); + }, + deps: [PlatformUtilsService], + }), + safeProvider({ + provide: DerivedStateProvider, + useClass: ForegroundDerivedStateProvider, + deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], + }), + safeProvider({ + provide: AutofillSettingsServiceAbstraction, + useClass: AutofillSettingsService, + deps: [StateProvider, PolicyService], + }), + safeProvider({ + provide: UserNotificationSettingsServiceAbstraction, + useClass: UserNotificationSettingsService, + deps: [StateProvider], + }), +]; + @NgModule({ imports: [JslibServicesModule], declarations: [], - providers: [ - InitService, - DebounceNavigationService, - DialogService, - PopupCloseWarningService, - { - provide: APP_INITIALIZER, - useFactory: (initService: InitService) => initService.init(), - deps: [InitService], - multi: true, - }, - { provide: BaseUnauthGuardService, useClass: UnauthGuardService }, - { - provide: MessagingService, - useFactory: () => { - return needsBackgroundInit - ? new BrowserMessagingPrivateModePopupService() - : new BrowserMessagingService(); - }, - }, - { - provide: TwoFactorService, - useFactory: getBgService("twoFactorService"), - deps: [], - }, - { - provide: AuthServiceAbstraction, - useFactory: getBgService("authService"), - deps: [], - }, - { - provide: LoginStrategyServiceAbstraction, - useFactory: getBgService("loginStrategyService"), - }, - { - provide: SsoLoginServiceAbstraction, - useFactory: getBgService("ssoLoginService"), - deps: [], - }, - { - provide: SearchServiceAbstraction, - useFactory: (logService: ConsoleLogService, i18nService: I18nServiceAbstraction) => { - return new PopupSearchService( - getBgService("searchService")(), - logService, - i18nService, - ); - }, - deps: [LogService, I18nServiceAbstraction], - }, - { - provide: CipherFileUploadService, - useFactory: getBgService("cipherFileUploadService"), - deps: [], - }, - { provide: CipherService, useFactory: getBgService("cipherService"), deps: [] }, - { - provide: CryptoFunctionService, - useFactory: () => new WebCryptoFunctionService(window), - deps: [], - }, - { - provide: CollectionService, - useFactory: getBgService("collectionService"), - deps: [], - }, - { - provide: LogService, - useFactory: (platformUtilsService: PlatformUtilsService) => - new ConsoleLogService(platformUtilsService.isDev()), - deps: [PlatformUtilsService], - }, - { - provide: BrowserEnvironmentService, - useClass: BrowserEnvironmentService, - deps: [LogService, StateProvider, AccountServiceAbstraction], - }, - { - provide: EnvironmentService, - useExisting: BrowserEnvironmentService, - }, - { provide: TotpService, useFactory: getBgService("totpService"), deps: [] }, - { - provide: I18nServiceAbstraction, - useFactory: (globalStateProvider: GlobalStateProvider) => { - return new I18nService(BrowserApi.getUILanguage(), globalStateProvider); - }, - deps: [GlobalStateProvider], - }, - { - provide: CryptoService, - useFactory: (encryptService: EncryptService) => { - const cryptoService = getBgService("cryptoService")(); - new ContainerService(cryptoService, encryptService).attachToGlobal(self); - return cryptoService; - }, - deps: [EncryptService], - }, - { - provide: AuthRequestServiceAbstraction, - useFactory: getBgService("authRequestService"), - deps: [], - }, - { - provide: DeviceTrustCryptoServiceAbstraction, - useFactory: getBgService("deviceTrustCryptoService"), - deps: [], - }, - { - provide: DevicesServiceAbstraction, - useFactory: getBgService("devicesService"), - deps: [], - }, - { - provide: PlatformUtilsService, - useExisting: ForegroundPlatformUtilsService, - }, - { - provide: ForegroundPlatformUtilsService, - useClass: ForegroundPlatformUtilsService, - useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { - return new ForegroundPlatformUtilsService( - sanitizer, - toastrService, - (clipboardValue: string, clearMs: number) => { - void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); - }, - async () => { - const response = await BrowserApi.sendMessageWithResponse<{ - result: boolean; - error: string; - }>("biometricUnlock"); - if (!response.result) { - throw response.error; - } - return response.result; - }, - window, - ); - }, - deps: [DomSanitizer, ToastrService], - }, - { - provide: PasswordGenerationServiceAbstraction, - useFactory: getBgService("passwordGenerationService"), - deps: [], - }, - { provide: SyncService, useFactory: getBgService("syncService"), deps: [] }, - { - provide: DomainSettingsService, - useClass: DefaultDomainSettingsService, - deps: [StateProvider], - }, - { - provide: AbstractStorageService, - useClass: BrowserLocalStorageService, - deps: [], - }, - { - provide: AutofillService, - useFactory: getBgService("autofillService"), - deps: [], - }, - { - provide: VaultExportServiceAbstraction, - useFactory: getBgService("exportService"), - deps: [], - }, - { - provide: KeyConnectorService, - useFactory: getBgService("keyConnectorService"), - deps: [], - }, - { - provide: UserVerificationService, - useFactory: getBgService("userVerificationService"), - deps: [], - }, - { - provide: VaultTimeoutSettingsService, - useFactory: getBgService("vaultTimeoutSettingsService"), - deps: [], - }, - { - provide: VaultTimeoutService, - useFactory: getBgService("vaultTimeoutService"), - deps: [], - }, - { - provide: NotificationsService, - useFactory: getBgService("notificationsService"), - deps: [], - }, - { - provide: VaultFilterService, - useClass: VaultFilterService, - deps: [ - OrganizationService, - FolderServiceAbstraction, - CipherService, - CollectionService, - PolicyService, - StateProvider, - AccountServiceAbstraction, - ], - }, - { - provide: SECURE_STORAGE, - useExisting: AbstractStorageService, // Secure storage is not available in the browser, so we use normal storage instead and warn users when it is used. - }, - { - provide: MEMORY_STORAGE, - useFactory: getBgService("memoryStorageService"), - }, - { - provide: OBSERVABLE_MEMORY_STORAGE, - useClass: ForegroundMemoryStorageService, - deps: [], - }, - { - provide: OBSERVABLE_DISK_STORAGE, - useExisting: AbstractStorageService, - }, - { - provide: StateServiceAbstraction, - useFactory: ( - storageService: AbstractStorageService, - secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, - logService: LogService, - accountService: AccountServiceAbstraction, - environmentService: EnvironmentService, - tokenService: TokenService, - migrationRunner: MigrationRunner, - ) => { - return new BrowserStateService( - storageService, - secureStorageService, - memoryStorageService, - logService, - new StateFactory(GlobalState, Account), - accountService, - environmentService, - tokenService, - migrationRunner, - ); - }, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - AccountServiceAbstraction, - EnvironmentService, - TokenService, - MigrationRunner, - ], - }, - { - provide: UsernameGenerationServiceAbstraction, - useFactory: getBgService("usernameGenerationService"), - deps: [], - }, - { - provide: BaseStateServiceAbstraction, - useExisting: StateServiceAbstraction, - deps: [], - }, - { - provide: FileDownloadService, - useClass: BrowserFileDownloadService, - }, - { - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useFactory: (platformUtilsService: PlatformUtilsService) => { - // Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light. - // In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed. - let windowContext = window; - const backgroundWindow = BrowserApi.getBackgroundPage(); - if (platformUtilsService.isSafari() && backgroundWindow) { - windowContext = backgroundWindow; - } - - return AngularThemingService.createSystemThemeFromWindow(windowContext); - }, - deps: [PlatformUtilsService], - }, - { - provide: FilePopoutUtilsService, - useFactory: (platformUtilsService: PlatformUtilsService) => { - return new FilePopoutUtilsService(platformUtilsService); - }, - deps: [PlatformUtilsService], - }, - { - provide: DerivedStateProvider, - useClass: ForegroundDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], - }, - { - provide: AutofillSettingsServiceAbstraction, - useClass: AutofillSettingsService, - deps: [StateProvider, PolicyService], - }, - { - provide: UserNotificationSettingsServiceAbstraction, - useClass: UserNotificationSettingsService, - deps: [StateProvider], - }, - ], + // Do not register your dependency here! Add it to the typesafeProviders array using the helper function + providers: safeProviders, }) export class ServicesModule {} diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index 65ce49cda940..4ab1d2ae2a18 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -4,7 +4,7 @@ import { Constructor, Opaque } from "type-fest"; import { SafeInjectionToken } from "../../services/injection-tokens"; /** - * The return type of our dependency helper functions. + * The return type of the {@link safeProvider} helper function. * Used to distinguish a type safe provider definition from a non-type safe provider definition. */ export type SafeProvider = Opaque; @@ -18,12 +18,22 @@ type MapParametersToDeps = { type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; +/** + * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken + */ +type ProviderInstanceType = + T extends SafeInjectionToken + ? InstanceType> + : T extends Constructor | AbstractConstructor + ? InstanceType + : never; + /** * Represents a dependency provided with the useClass option. */ type SafeClassProvider< - A extends AbstractConstructor, - I extends Constructor>, + A extends AbstractConstructor | SafeInjectionToken, + I extends Constructor>, D extends MapParametersToDeps>, > = { provide: A; @@ -40,37 +50,25 @@ type SafeValueProvider, V extends SafeInjectio }; /** - * Represents a dependency provided with the useFactory option where a SafeInjectionToken is used as the token. - */ -type SafeFactoryProviderWithToken< - A extends SafeInjectionToken, - I extends (...args: any) => InstanceType>, - D extends MapParametersToDeps>, -> = { - provide: A; - useFactory: I; - deps: D; -}; - -/** - * Represents a dependency provided with the useFactory option where an abstract class is used as the token. + * Represents a dependency provided with the useFactory option. */ -type SafeFactoryProviderWithClass< - A extends AbstractConstructor, - I extends (...args: any) => InstanceType, +type SafeFactoryProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends (...args: any) => ProviderInstanceType, D extends MapParametersToDeps>, > = { provide: A; useFactory: I; deps: D; + multi?: boolean; }; /** * Represents a dependency provided with the useExisting option. */ type SafeExistingProvider< - A extends Constructor | AbstractConstructor, - I extends Constructor> | AbstractConstructor>, + A extends Constructor | AbstractConstructor | SafeInjectionToken, + I extends Constructor> | AbstractConstructor>, > = { provide: A; useExisting: I; @@ -84,31 +82,26 @@ type SafeExistingProvider< */ export const safeProvider = < // types for useClass - AClass extends AbstractConstructor, - IClass extends Constructor>, + AClass extends AbstractConstructor | SafeInjectionToken, + IClass extends Constructor>, DClass extends MapParametersToDeps>, // types for useValue AValue extends SafeInjectionToken, VValue extends SafeInjectionTokenType, - // types for useFactoryWithToken - AFactoryToken extends SafeInjectionToken, - IFactoryToken extends (...args: any) => InstanceType>, - DFactoryToken extends MapParametersToDeps>, - // types for useFactoryWithClass - AFactoryClass extends AbstractConstructor, - IFactoryClass extends (...args: any) => InstanceType, - DFactoryClass extends MapParametersToDeps>, + // types for useFactory + AFactory extends AbstractConstructor | SafeInjectionToken, + IFactory extends (...args: any) => ProviderInstanceType, + DFactory extends MapParametersToDeps>, // types for useExisting - AExisting extends Constructor | AbstractConstructor, + AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, IExisting extends - | Constructor> - | AbstractConstructor>, + | Constructor> + | AbstractConstructor>, >( provider: | SafeClassProvider | SafeValueProvider - | SafeFactoryProviderWithToken - | SafeFactoryProviderWithClass + | SafeFactoryProvider | SafeExistingProvider | Constructor, ): SafeProvider => provider as SafeProvider; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 67d38d33de0f..521387181bd3 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,5 +1,4 @@ import { LOCALE_ID, NgModule } from "@angular/core"; -import { UnwrapOpaque } from "type-fest"; import { AuthRequestServiceAbstraction, @@ -267,7 +266,7 @@ import { ModalService } from "./modal.service"; * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. * If you need help please ask for it, do NOT change the type of this array. */ -const typesafeProviders: Array = [ +const safeProviders: SafeProvider[] = [ safeProvider(AuthGuard), safeProvider(UnauthGuard), safeProvider(ModalService), @@ -1085,6 +1084,6 @@ function encryptServiceFactory( @NgModule({ declarations: [], // Do not register your dependency here! Add it to the typesafeProviders array using the helper function - providers: typesafeProviders as UnwrapOpaque[], + providers: safeProviders, }) export class JslibServicesModule {} From d10c14791d8905251e28fb47ae3986c36193e1f6 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:44:08 +1000 Subject: [PATCH 32/51] [AC-2329] [BEEEP] Use safeProvider in desktop services module (#8457) --- .../src/app/services/services.module.ts | 328 ++++++++++-------- .../native-message-handler.service.ts | 2 +- .../src/platform/utils/safe-provider.ts | 15 + 3 files changed, 198 insertions(+), 147 deletions(-) diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 495d6abcf151..1d75ff4ca90e 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,8 +1,8 @@ -import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core"; +import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, - STATE_FACTORY, STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, @@ -12,6 +12,8 @@ import { WINDOW, SUPPORTS_SECURE_STORAGE, SYSTEM_THEME_OBSERVABLE, + SafeInjectionToken, + STATE_FACTORY, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -77,153 +79,187 @@ import { DesktopFileDownloadService } from "./desktop-file-download.service"; import { InitService } from "./init.service"; import { RendererCryptoFunctionService } from "./renderer-crypto-function.service"; -const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); +const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); + +// Desktop has its own Account definition which must be used in its StateService +const DESKTOP_STATE_FACTORY = new SafeInjectionToken>( + "DESKTOP_STATE_FACTORY", +); + +/** + * Provider definitions used in the ngModule. + * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. + * If you need help please ask for it, do NOT change the type of this array. + */ +const safeProviders: SafeProvider[] = [ + safeProvider(InitService), + safeProvider(NativeMessagingService), + safeProvider(SearchBarService), + safeProvider(LoginGuard), + safeProvider(DialogService), + safeProvider({ + provide: APP_INITIALIZER as SafeInjectionToken<() => void>, + useFactory: (initService: InitService) => initService.init(), + deps: [InitService], + multi: true, + }), + safeProvider({ + provide: DESKTOP_STATE_FACTORY, + useValue: new StateFactory(GlobalState, Account), + }), + safeProvider({ + provide: STATE_FACTORY, + useValue: null, + }), + safeProvider({ + provide: RELOAD_CALLBACK, + useValue: null, + }), + safeProvider({ + provide: LogServiceAbstraction, + useClass: ElectronLogRendererService, + deps: [], + }), + safeProvider({ + provide: PlatformUtilsServiceAbstraction, + useClass: ElectronPlatformUtilsService, + deps: [I18nServiceAbstraction, MessagingServiceAbstraction], + }), + safeProvider({ + // We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid + // the TokenService having to inject the PlatformUtilsService which introduces a + // circular dependency on Desktop only. + provide: SUPPORTS_SECURE_STORAGE, + useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, + }), + safeProvider({ + provide: I18nServiceAbstraction, + useClass: I18nRendererService, + deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], + }), + safeProvider({ + provide: MessagingServiceAbstraction, + useClass: ElectronRendererMessagingService, + deps: [BroadcasterServiceAbstraction], + }), + safeProvider({ + provide: AbstractStorageService, + useClass: ElectronRendererStorageService, + deps: [], + }), + safeProvider({ + provide: SECURE_STORAGE, + useClass: ElectronRendererSecureStorageService, + deps: [], + }), + safeProvider({ provide: MEMORY_STORAGE, useClass: MemoryStorageService, deps: [] }), + safeProvider({ + provide: OBSERVABLE_MEMORY_STORAGE, + useClass: MemoryStorageServiceForStateProviders, + deps: [], + }), + safeProvider({ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService }), + safeProvider({ + provide: SystemServiceAbstraction, + useClass: SystemService, + deps: [ + MessagingServiceAbstraction, + PlatformUtilsServiceAbstraction, + RELOAD_CALLBACK, + StateServiceAbstraction, + AutofillSettingsServiceAbstraction, + VaultTimeoutSettingsService, + BiometricStateService, + ], + }), + safeProvider({ + provide: StateServiceAbstraction, + useClass: ElectronStateService, + deps: [ + AbstractStorageService, + SECURE_STORAGE, + MEMORY_STORAGE, + LogService, + DESKTOP_STATE_FACTORY, + AccountServiceAbstraction, + EnvironmentService, + TokenService, + MigrationRunner, + STATE_SERVICE_USE_CACHE, + ], + }), + safeProvider({ + provide: FileDownloadService, + useClass: DesktopFileDownloadService, + deps: [], + }), + safeProvider({ + provide: SYSTEM_THEME_OBSERVABLE, + useFactory: () => fromIpcSystemTheme(), + deps: [], + }), + safeProvider({ + provide: EncryptedMessageHandlerService, + deps: [ + StateServiceAbstraction, + AuthServiceAbstraction, + CipherServiceAbstraction, + PolicyServiceAbstraction, + MessagingServiceAbstraction, + PasswordGenerationServiceAbstraction, + ], + }), + safeProvider({ + provide: NativeMessageHandlerService, + deps: [ + StateServiceAbstraction, + CryptoServiceAbstraction, + CryptoFunctionServiceAbstraction, + MessagingServiceAbstraction, + EncryptedMessageHandlerService, + DialogService, + DesktopAutofillSettingsService, + ], + }), + safeProvider({ + provide: LoginServiceAbstraction, + useClass: LoginService, + deps: [StateServiceAbstraction], + }), + safeProvider({ + provide: CryptoFunctionServiceAbstraction, + useClass: RendererCryptoFunctionService, + deps: [WINDOW], + }), + safeProvider({ + provide: CryptoServiceAbstraction, + useClass: ElectronCryptoService, + deps: [ + KeyGenerationServiceAbstraction, + CryptoFunctionServiceAbstraction, + EncryptService, + PlatformUtilsServiceAbstraction, + LogService, + StateServiceAbstraction, + AccountServiceAbstraction, + StateProvider, + BiometricStateService, + ], + }), + safeProvider({ + provide: DesktopSettingsService, + deps: [StateProvider], + }), + safeProvider({ + provide: DesktopAutofillSettingsService, + deps: [StateProvider], + }), +]; @NgModule({ imports: [JslibServicesModule], declarations: [], - providers: [ - InitService, - NativeMessagingService, - SearchBarService, - LoginGuard, - DialogService, - { - provide: APP_INITIALIZER, - useFactory: (initService: InitService) => initService.init(), - deps: [InitService], - multi: true, - }, - { - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }, - { - provide: RELOAD_CALLBACK, - useValue: null, - }, - { provide: LogServiceAbstraction, useClass: ElectronLogRendererService, deps: [] }, - { - provide: PlatformUtilsServiceAbstraction, - useClass: ElectronPlatformUtilsService, - deps: [I18nServiceAbstraction, MessagingServiceAbstraction], - }, - { - // We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid - // the TokenService having to inject the PlatformUtilsService which introduces a - // circular dependency on Desktop only. - provide: SUPPORTS_SECURE_STORAGE, - useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, - }, - { - provide: I18nServiceAbstraction, - useClass: I18nRendererService, - deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], - }, - { - provide: MessagingServiceAbstraction, - useClass: ElectronRendererMessagingService, - deps: [BroadcasterServiceAbstraction], - }, - { provide: AbstractStorageService, useClass: ElectronRendererStorageService }, - { provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService }, - { provide: MEMORY_STORAGE, useClass: MemoryStorageService }, - { provide: OBSERVABLE_MEMORY_STORAGE, useClass: MemoryStorageServiceForStateProviders }, - { provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService }, - { - provide: SystemServiceAbstraction, - useClass: SystemService, - deps: [ - MessagingServiceAbstraction, - PlatformUtilsServiceAbstraction, - RELOAD_CALLBACK, - StateServiceAbstraction, - AutofillSettingsServiceAbstraction, - VaultTimeoutSettingsService, - BiometricStateService, - ], - }, - { - provide: StateServiceAbstraction, - useClass: ElectronStateService, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - STATE_FACTORY, - AccountServiceAbstraction, - EnvironmentService, - TokenService, - MigrationRunner, - STATE_SERVICE_USE_CACHE, - ], - }, - { - provide: FileDownloadService, - useClass: DesktopFileDownloadService, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useFactory: () => fromIpcSystemTheme(), - }, - { - provide: EncryptedMessageHandlerService, - deps: [ - StateServiceAbstraction, - AuthServiceAbstraction, - CipherServiceAbstraction, - PolicyServiceAbstraction, - MessagingServiceAbstraction, - PasswordGenerationServiceAbstraction, - ], - }, - { - provide: NativeMessageHandlerService, - deps: [ - StateServiceAbstraction, - CryptoServiceAbstraction, - CryptoFunctionServiceAbstraction, - MessagingServiceAbstraction, - EncryptedMessageHandlerService, - DialogService, - ], - }, - { - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }, - { - provide: CryptoFunctionServiceAbstraction, - useClass: RendererCryptoFunctionService, - deps: [WINDOW], - }, - { - provide: CryptoServiceAbstraction, - useClass: ElectronCryptoService, - deps: [ - KeyGenerationServiceAbstraction, - CryptoFunctionServiceAbstraction, - EncryptService, - PlatformUtilsServiceAbstraction, - LogService, - StateServiceAbstraction, - AccountServiceAbstraction, - StateProvider, - BiometricStateService, - ], - }, - { - provide: DesktopSettingsService, - useClass: DesktopSettingsService, - deps: [StateProvider], - }, - { - provide: DesktopAutofillSettingsService, - useClass: DesktopAutofillSettingsService, - deps: [StateProvider], - }, - ], + // Do not register your dependency here! Add it to the typesafeProviders array using the helper function + providers: safeProviders, }) export class ServicesModule {} diff --git a/apps/desktop/src/services/native-message-handler.service.ts b/apps/desktop/src/services/native-message-handler.service.ts index 785b65195a0d..ebe1ee62484a 100644 --- a/apps/desktop/src/services/native-message-handler.service.ts +++ b/apps/desktop/src/services/native-message-handler.service.ts @@ -5,10 +5,10 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { DialogService } from "@bitwarden/components"; import { VerifyNativeMessagingDialogComponent } from "../app/components/verify-native-messaging-dialog.component"; diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index 4ab1d2ae2a18..7c19a280d621 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -74,6 +74,17 @@ type SafeExistingProvider< useExisting: I; }; +/** + * Represents a dependency where there is no abstract token, the token is the implementation + */ +type SafeConcreteProvider< + I extends Constructor, + D extends MapParametersToDeps>, +> = { + provide: I; + deps: D; +}; + /** * A factory function that creates a provider for the ngModule providers array. * This guarantees type safety for your provider definition. It does nothing at runtime. @@ -97,11 +108,15 @@ export const safeProvider = < IExisting extends | Constructor> | AbstractConstructor>, + // types for no token + IConcrete extends Constructor, + DConcrete extends MapParametersToDeps>, >( provider: | SafeClassProvider | SafeValueProvider | SafeFactoryProvider | SafeExistingProvider + | SafeConcreteProvider | Constructor, ): SafeProvider => provider as SafeProvider; From 2edc156dd64544fb13b420d22e1a0151e491aaf8 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 28 Mar 2024 12:01:09 +0100 Subject: [PATCH 33/51] [STRICT TS] Migrate platform abstract services functions (#8527) We currently use a callback syntax for abstract services. This syntax isn't completely strict compliant and will fail the strictPropertyInitialization check. We also currently don't get any compile time errors if we forget to implement a function. To that end this PR updates all platform owned services to use the appropriate abstract keyword for non implemented functions. I also updated the fields to be actual functions and not properties. --- .../biometrics.service.abstraction.ts | 32 ++-- .../form-validation-errors.service.ts | 2 +- .../theming/theming.service.abstraction.ts | 4 +- .../platform/abstractions/app-id.service.ts | 8 +- .../abstractions/broadcaster.service.ts | 6 +- .../config/config-api.service.abstraction.ts | 2 +- .../abstractions/crypto-function.service.ts | 62 +++---- .../platform/abstractions/crypto.service.ts | 161 ++++++++++-------- .../platform/abstractions/encrypt.service.ts | 23 +-- .../file-download/file-download.service.ts | 2 +- .../file-upload/file-upload.service.ts | 4 +- .../src/platform/abstractions/i18n.service.ts | 4 +- .../abstractions/key-generation.service.ts | 14 +- .../src/platform/abstractions/log.service.ts | 10 +- .../abstractions/messaging.service.ts | 2 +- .../abstractions/platform-utils.service.ts | 52 +++--- .../platform/abstractions/system.service.ts | 8 +- .../abstractions/translation.service.ts | 12 +- .../abstractions/validation.service.ts | 2 +- .../biometrics/biometric-state.service.ts | 14 +- .../platform/state/derived-state.provider.ts | 4 +- .../platform/state/global-state.provider.ts | 2 +- .../src/platform/state/state.provider.ts | 8 +- .../src/platform/state/user-state.provider.ts | 2 +- .../platform/theming/theme-state.service.ts | 4 +- 25 files changed, 232 insertions(+), 212 deletions(-) diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts index 2d5c1d19ebb7..fb7ce048b5a6 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts @@ -1,6 +1,6 @@ export abstract class BiometricsServiceAbstraction { - osSupportsBiometric: () => Promise; - canAuthBiometric: ({ + abstract osSupportsBiometric(): Promise; + abstract canAuthBiometric({ service, key, userId, @@ -8,11 +8,11 @@ export abstract class BiometricsServiceAbstraction { service: string; key: string; userId: string; - }) => Promise; - authenticateBiometric: () => Promise; - getBiometricKey: (service: string, key: string) => Promise; - setBiometricKey: (service: string, key: string, value: string) => Promise; - setEncryptionKeyHalf: ({ + }): Promise; + abstract authenticateBiometric(): Promise; + abstract getBiometricKey(service: string, key: string): Promise; + abstract setBiometricKey(service: string, key: string, value: string): Promise; + abstract setEncryptionKeyHalf({ service, key, value, @@ -20,23 +20,23 @@ export abstract class BiometricsServiceAbstraction { service: string; key: string; value: string; - }) => void; - deleteBiometricKey: (service: string, key: string) => Promise; + }): void; + abstract deleteBiometricKey(service: string, key: string): Promise; } export interface OsBiometricService { - osSupportsBiometric: () => Promise; - authenticateBiometric: () => Promise; - getBiometricKey: ( + osSupportsBiometric(): Promise; + authenticateBiometric(): Promise; + getBiometricKey( service: string, key: string, clientKeyHalfB64: string | undefined, - ) => Promise; - setBiometricKey: ( + ): Promise; + setBiometricKey( service: string, key: string, value: string, clientKeyHalfB64: string | undefined, - ) => Promise; - deleteBiometricKey: (service: string, key: string) => Promise; + ): Promise; + deleteBiometricKey(service: string, key: string): Promise; } diff --git a/libs/angular/src/platform/abstractions/form-validation-errors.service.ts b/libs/angular/src/platform/abstractions/form-validation-errors.service.ts index 08a12443a0c8..266afff5f340 100644 --- a/libs/angular/src/platform/abstractions/form-validation-errors.service.ts +++ b/libs/angular/src/platform/abstractions/form-validation-errors.service.ts @@ -9,5 +9,5 @@ export interface FormGroupControls { } export abstract class FormValidationErrorsService { - getFormValidationErrors: (controls: FormGroupControls) => AllValidationErrors[]; + abstract getFormValidationErrors(controls: FormGroupControls): AllValidationErrors[]; } diff --git a/libs/angular/src/platform/services/theming/theming.service.abstraction.ts b/libs/angular/src/platform/services/theming/theming.service.abstraction.ts index 9a012a7f75bb..4306d312c5eb 100644 --- a/libs/angular/src/platform/services/theming/theming.service.abstraction.ts +++ b/libs/angular/src/platform/services/theming/theming.service.abstraction.ts @@ -11,12 +11,12 @@ export abstract class AbstractThemingService { * The effective theme based on the user configured choice and the current system theme if * the configured choice is {@link ThemeType.System}. */ - theme$: Observable; + abstract theme$: Observable; /** * Listens for effective theme changes and applies changes to the provided document. * @param document The document that should have theme classes applied to it. * * @returns A subscription that can be unsubscribed from to cancel the application of theme classes. */ - applyThemeChangesTo: (document: Document) => Subscription; + abstract applyThemeChangesTo(document: Document): Subscription; } diff --git a/libs/common/src/platform/abstractions/app-id.service.ts b/libs/common/src/platform/abstractions/app-id.service.ts index c1414dd01ffe..c2c1a23ef5e2 100644 --- a/libs/common/src/platform/abstractions/app-id.service.ts +++ b/libs/common/src/platform/abstractions/app-id.service.ts @@ -1,8 +1,8 @@ import { Observable } from "rxjs"; export abstract class AppIdService { - appId$: Observable; - anonymousAppId$: Observable; - getAppId: () => Promise; - getAnonymousAppId: () => Promise; + abstract appId$: Observable; + abstract anonymousAppId$: Observable; + abstract getAppId(): Promise; + abstract getAnonymousAppId(): Promise; } diff --git a/libs/common/src/platform/abstractions/broadcaster.service.ts b/libs/common/src/platform/abstractions/broadcaster.service.ts index 5df3c033433f..8abfb5a90c53 100644 --- a/libs/common/src/platform/abstractions/broadcaster.service.ts +++ b/libs/common/src/platform/abstractions/broadcaster.service.ts @@ -9,13 +9,13 @@ export abstract class BroadcasterService { /** * @deprecated Use the observable from the appropriate service instead. */ - send: (message: MessageBase, id?: string) => void; + abstract send(message: MessageBase, id?: string): void; /** * @deprecated Use the observable from the appropriate service instead. */ - subscribe: (id: string, messageCallback: (message: MessageBase) => void) => void; + abstract subscribe(id: string, messageCallback: (message: MessageBase) => void): void; /** * @deprecated Use the observable from the appropriate service instead. */ - unsubscribe: (id: string) => void; + abstract unsubscribe(id: string): void; } diff --git a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts index 63534becf343..3c191f59ccc9 100644 --- a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts @@ -5,5 +5,5 @@ export abstract class ConfigApiServiceAbstraction { /** * Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context. */ - get: (userId: UserId | undefined) => Promise; + abstract get(userId: UserId | undefined): Promise; } diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index db432abc34ca..18c14677dd09 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -3,85 +3,85 @@ import { DecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoFunctionService { - pbkdf2: ( + abstract pbkdf2( password: string | Uint8Array, salt: string | Uint8Array, algorithm: "sha256" | "sha512", iterations: number, - ) => Promise; - argon2: ( + ): Promise; + abstract argon2( password: string | Uint8Array, salt: string | Uint8Array, iterations: number, memory: number, parallelism: number, - ) => Promise; - hkdf: ( + ): Promise; + abstract hkdf( ikm: Uint8Array, salt: string | Uint8Array, info: string | Uint8Array, outputByteSize: number, algorithm: "sha256" | "sha512", - ) => Promise; - hkdfExpand: ( + ): Promise; + abstract hkdfExpand( prk: Uint8Array, info: string | Uint8Array, outputByteSize: number, algorithm: "sha256" | "sha512", - ) => Promise; - hash: ( + ): Promise; + abstract hash( value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512" | "md5", - ) => Promise; - hmac: ( + ): Promise; + abstract hmac( value: Uint8Array, key: Uint8Array, algorithm: "sha1" | "sha256" | "sha512", - ) => Promise; - compare: (a: Uint8Array, b: Uint8Array) => Promise; - hmacFast: ( + ): Promise; + abstract compare(a: Uint8Array, b: Uint8Array): Promise; + abstract hmacFast( value: Uint8Array | string, key: Uint8Array | string, algorithm: "sha1" | "sha256" | "sha512", - ) => Promise; - compareFast: (a: Uint8Array | string, b: Uint8Array | string) => Promise; - aesEncrypt: (data: Uint8Array, iv: Uint8Array, key: Uint8Array) => Promise; - aesDecryptFastParameters: ( + ): Promise; + abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise; + abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise; + abstract aesDecryptFastParameters( data: string, iv: string, mac: string, key: SymmetricCryptoKey, - ) => DecryptParameters; - aesDecryptFast: ( + ): DecryptParameters; + abstract aesDecryptFast( parameters: DecryptParameters, mode: "cbc" | "ecb", - ) => Promise; - aesDecrypt: ( + ): Promise; + abstract aesDecrypt( data: Uint8Array, iv: Uint8Array, key: Uint8Array, mode: "cbc" | "ecb", - ) => Promise; - rsaEncrypt: ( + ): Promise; + abstract rsaEncrypt( data: Uint8Array, publicKey: Uint8Array, algorithm: "sha1" | "sha256", - ) => Promise; - rsaDecrypt: ( + ): Promise; + abstract rsaDecrypt( data: Uint8Array, privateKey: Uint8Array, algorithm: "sha1" | "sha256", - ) => Promise; - rsaExtractPublicKey: (privateKey: Uint8Array) => Promise; - rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[Uint8Array, Uint8Array]>; + ): Promise; + abstract rsaExtractPublicKey(privateKey: Uint8Array): Promise; + abstract rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]>; /** * Generates a key of the given length suitable for use in AES encryption */ - aesGenerateKey: (bitLength: 128 | 192 | 256 | 512) => Promise; + abstract aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise; /** * Generates a random array of bytes of the given length. Uses a cryptographically secure random number generator. * * Do not use this for generating encryption keys. Use aesGenerateKey or rsaGenerateKeyPair instead. */ - randomBytes: (length: number) => Promise; + abstract randomBytes(length: number): Promise; } diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index a5a2d452334e..44ff52168091 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -12,7 +12,7 @@ import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoService { - activeUserKey$: Observable; + abstract activeUserKey$: Observable; /** * Sets the provided user key and stores * any other necessary versions (such as auto, biometrics, @@ -22,105 +22,105 @@ export abstract class CryptoService { * @param key The user key to set * @param userId The desired user */ - setUserKey: (key: UserKey, userId?: string) => Promise; + abstract setUserKey(key: UserKey, userId?: string): Promise; /** * Gets the user key from memory and sets it again, * kicking off a refresh of any additional keys * (such as auto, biometrics, or pin) */ - refreshAdditionalKeys: () => Promise; + abstract refreshAdditionalKeys(): Promise; /** * Observable value that returns whether or not the currently active user has ever had auser key, * i.e. has ever been unlocked/decrypted. This is key for differentiating between TDE locked and standard locked states. */ - everHadUserKey$: Observable; + abstract everHadUserKey$: Observable; /** * Retrieves the user key * @param userId The desired user * @returns The user key */ - getUserKey: (userId?: string) => Promise; + abstract getUserKey(userId?: string): Promise; /** * Checks if the user is using an old encryption scheme that used the master key * for encryption of data instead of the user key. */ - isLegacyUser: (masterKey?: MasterKey, userId?: string) => Promise; + abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise; /** * Use for encryption/decryption of data in order to support legacy * encryption models. It will return the user key if available, * if not it will return the master key. * @param userId The desired user */ - getUserKeyWithLegacySupport: (userId?: string) => Promise; + abstract getUserKeyWithLegacySupport(userId?: string): Promise; /** * Retrieves the user key from storage * @param keySuffix The desired version of the user's key to retrieve * @param userId The desired user * @returns The user key */ - getUserKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * Determines whether the user key is available for the given user. * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false. * @returns True if the user key is available */ - hasUserKey: (userId?: UserId) => Promise; + abstract hasUserKey(userId?: UserId): Promise; /** * Determines whether the user key is available for the given user in memory. * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false. * @returns True if the user key is available */ - hasUserKeyInMemory: (userId?: string) => Promise; + abstract hasUserKeyInMemory(userId?: string): Promise; /** * @param keySuffix The desired version of the user's key to check * @param userId The desired user * @returns True if the provided version of the user key is stored */ - hasUserKeyStored: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * Generates a new user key * @param masterKey The user's master key * @returns A new user key and the master key protected version of it */ - makeUserKey: (key: MasterKey) => Promise<[UserKey, EncString]>; + abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>; /** * Clears the user key * @param clearStoredKeys Clears all stored versions of the user keys as well, * such as the biometrics key * @param userId The desired user */ - clearUserKey: (clearSecretStorage?: boolean, userId?: string) => Promise; + abstract clearUserKey(clearSecretStorage?: boolean, userId?: string): Promise; /** * Clears the user's stored version of the user key * @param keySuffix The desired version of the key to clear * @param userId The desired user */ - clearStoredUserKey: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * Stores the master key encrypted user key * @param userKeyMasterKey The master key encrypted user key to set * @param userId The desired user */ - setMasterKeyEncryptedUserKey: (UserKeyMasterKey: string, userId?: string) => Promise; + abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise; /** * Sets the user's master key * @param key The user's master key to set * @param userId The desired user */ - setMasterKey: (key: MasterKey, userId?: string) => Promise; + abstract setMasterKey(key: MasterKey, userId?: string): Promise; /** * @param userId The desired user * @returns The user's master key */ - getMasterKey: (userId?: string) => Promise; + abstract getMasterKey(userId?: string): Promise; /** * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user */ - getOrDeriveMasterKey: (password: string, userId?: string) => Promise; + abstract getOrDeriveMasterKey(password: string, userId?: string): Promise; /** * Generates a master key from the provided password * @param password The user's master password @@ -129,17 +129,17 @@ export abstract class CryptoService { * @param KdfConfig The user's key derivation function configuration * @returns A master key derived from the provided password */ - makeMasterKey: ( + abstract makeMasterKey( password: string, email: string, kdf: KdfType, KdfConfig: KdfConfig, - ) => Promise; + ): Promise; /** * Clears the user's master key * @param userId The desired user */ - clearMasterKey: (userId?: string) => Promise; + abstract clearMasterKey(userId?: string): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -147,10 +147,10 @@ export abstract class CryptoService { * @param userKey The user key * @returns The user key and the master key protected version of it */ - encryptUserKeyWithMasterKey: ( + abstract encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, - ) => Promise<[UserKey, EncString]>; + ): Promise<[UserKey, EncString]>; /** * Decrypts the user key with the provided master key * @param masterKey The user's master key @@ -158,11 +158,11 @@ export abstract class CryptoService { * @param userId The desired user * @returns The user key */ - decryptUserKeyWithMasterKey: ( + abstract decryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: EncString, userId?: string, - ) => Promise; + ): Promise; /** * Creates a master password hash from the user's master password. Can * be used for local authentication or for server authentication depending @@ -172,21 +172,25 @@ export abstract class CryptoService { * @param hashPurpose The iterations to use for the hash * @returns The user's master password hash */ - hashMasterKey: (password: string, key: MasterKey, hashPurpose?: HashPurpose) => Promise; + abstract hashMasterKey( + password: string, + key: MasterKey, + hashPurpose?: HashPurpose, + ): Promise; /** * Sets the user's master password hash * @param keyHash The user's master password hash to set */ - setMasterKeyHash: (keyHash: string) => Promise; + abstract setMasterKeyHash(keyHash: string): Promise; /** * @returns The user's master password hash */ - getMasterKeyHash: () => Promise; + abstract getMasterKeyHash(): Promise; /** * Clears the user's stored master password hash * @param userId The desired user */ - clearMasterKeyHash: (userId?: string) => Promise; + abstract clearMasterKeyHash(userId?: string): Promise; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. @@ -195,107 +199,109 @@ export abstract class CryptoService { * @returns True if the provided master password matches either the stored * key hash or the server key hash */ - compareAndUpdateKeyHash: (masterPassword: string, masterKey: MasterKey) => Promise; + abstract compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise; /** * Stores the encrypted organization keys and clears any decrypted * organization keys currently in memory * @param orgs The organizations to set keys for * @param providerOrgs The provider organizations to set keys for */ - setOrgKeys: ( + abstract setOrgKeys( orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[], - ) => Promise; - activeUserOrgKeys$: Observable>; + ): Promise; + abstract activeUserOrgKeys$: Observable>; /** * Returns the organization's symmetric key * @deprecated Use the observable activeUserOrgKeys$ and `map` to the desired orgKey instead * @param orgId The desired organization * @returns The organization's symmetric key */ - getOrgKey: (orgId: string) => Promise; + abstract getOrgKey(orgId: string): Promise; /** * @deprecated Use the observable activeUserOrgKeys$ instead * @returns A record of the organization Ids to their symmetric keys */ - getOrgKeys: () => Promise>; + abstract getOrgKeys(): Promise>; /** * Uses the org key to derive a new symmetric key for encrypting data * @param orgKey The organization's symmetric key */ - makeDataEncKey: (key: T) => Promise<[SymmetricCryptoKey, EncString]>; + abstract makeDataEncKey( + key: T, + ): Promise<[SymmetricCryptoKey, EncString]>; /** * Clears the user's stored organization keys * @param memoryOnly Clear only the in-memory keys * @param userId The desired user */ - clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise; + abstract clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise; /** * Stores the encrypted provider keys and clears any decrypted * provider keys currently in memory * @param providers The providers to set keys for */ - activeUserProviderKeys$: Observable>; - setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise; + abstract activeUserProviderKeys$: Observable>; + abstract setProviderKeys(orgs: ProfileProviderResponse[]): Promise; /** * @param providerId The desired provider * @returns The provider's symmetric key */ - getProviderKey: (providerId: string) => Promise; + abstract getProviderKey(providerId: string): Promise; /** * @returns A record of the provider Ids to their symmetric keys */ - getProviderKeys: () => Promise>; + abstract getProviderKeys(): Promise>; /** * @param memoryOnly Clear only the in-memory keys * @param userId The desired user */ - clearProviderKeys: (memoryOnly?: boolean, userId?: string) => Promise; + abstract clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise; /** * Returns the public key from memory. If not available, extracts it * from the private key and stores it in memory * @returns The user's public key */ - getPublicKey: () => Promise; + abstract getPublicKey(): Promise; /** * Creates a new organization key and encrypts it with the user's public key. * This method can also return Provider keys for creating new Provider users. * @returns The new encrypted org key and the decrypted key itself */ - makeOrgKey: () => Promise<[EncString, T]>; + abstract makeOrgKey(): Promise<[EncString, T]>; /** * Sets the the user's encrypted private key in storage and * clears the decrypted private key from memory * Note: does not clear the private key if null is provided * @param encPrivateKey An encrypted private key */ - setPrivateKey: (encPrivateKey: string) => Promise; + abstract setPrivateKey(encPrivateKey: string): Promise; /** * Returns the private key from memory. If not available, decrypts it * from storage and stores it in memory * @returns The user's private key */ - getPrivateKey: () => Promise; + abstract getPrivateKey(): Promise; /** * Generates a fingerprint phrase for the user based on their public key * @param fingerprintMaterial Fingerprint material * @param publicKey The user's public key * @returns The user's fingerprint phrase */ - getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise; + abstract getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise; /** * Generates a new keypair * @param key A key to encrypt the private key with. If not provided, * defaults to the user key * @returns A new keypair: [publicKey in Base64, encrypted privateKey] */ - makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>; + abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>; /** * Clears the user's key pair * @param memoryOnly Clear only the in-memory keys * @param userId The desired user */ - clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise; + abstract clearKeyPair(memoryOnly?: boolean, userId?: string): Promise; /** * @param pin The user's pin * @param salt The user's salt @@ -303,14 +309,19 @@ export abstract class CryptoService { * @param kdfConfig The user's kdf config * @returns A key derived from the user's pin */ - makePinKey: (pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig) => Promise; + abstract makePinKey( + pin: string, + salt: string, + kdf: KdfType, + kdfConfig: KdfConfig, + ): Promise; /** * Clears the user's pin keys from storage * Note: This will remove the stored pin and as a result, * disable pin protection for the user * @param userId The desired user */ - clearPinKeys: (userId?: string) => Promise; + abstract clearPinKeys(userId?: string): Promise; /** * Decrypts the user key with their pin * @param pin The user's PIN @@ -321,13 +332,13 @@ export abstract class CryptoService { * it will be retrieved from storage * @returns The decrypted user key */ - decryptUserKeyWithPin: ( + abstract decryptUserKeyWithPin( pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, - ) => Promise; + ): Promise; /** * Creates a new Pin key that encrypts the user key instead of the * master key. Clears the old Pin key from state. @@ -340,55 +351,55 @@ export abstract class CryptoService { * places depending on if Master Password on Restart was enabled) * @returns The user key */ - decryptAndMigrateOldPinKey: ( + abstract decryptAndMigrateOldPinKey( masterPasswordOnRestart: boolean, pin: string, email: string, kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, - ) => Promise; + ): Promise; /** * Replaces old master auto keys with new user auto keys */ - migrateAutoKeyIfNeeded: (userId?: string) => Promise; + abstract migrateAutoKeyIfNeeded(userId?: string): Promise; /** * @param keyMaterial The key material to derive the send key from * @returns A new send key */ - makeSendKey: (keyMaterial: Uint8Array) => Promise; + abstract makeSendKey(keyMaterial: Uint8Array): Promise; /** * Clears all of the user's keys from storage * @param userId The user's Id */ - clearKeys: (userId?: string) => Promise; + abstract clearKeys(userId?: string): Promise; /** * RSA encrypts a value. * @param data The data to encrypt * @param publicKey The public key to use for encryption, if not provided, the user's public key will be used * @returns The encrypted data */ - rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise; + abstract rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise; /** * Decrypts a value using RSA. * @param encValue The encrypted value to decrypt * @param privateKeyValue The private key to use for decryption * @returns The decrypted value */ - rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise; - randomNumber: (min: number, max: number) => Promise; + abstract rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise; + abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key * @returns A new cipher key */ - makeCipherKey: () => Promise; + abstract makeCipherKey(): Promise; /** * Initialize all necessary crypto keys needed for a new account. * Warning! This completely replaces any existing keys! * @returns The user's newly created public key, private key, and encrypted private key */ - initAccount: () => Promise<{ + abstract initAccount(): Promise<{ userKey: UserKey; publicKey: string; privateKey: EncString; @@ -400,18 +411,18 @@ export abstract class CryptoService { * @remarks * Should always be called before updating a users KDF config. */ - validateKdfConfig: (kdf: KdfType, kdfConfig: KdfConfig) => void; + abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void; /** * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead. */ - decryptMasterKeyWithPin: ( + abstract decryptMasterKeyWithPin( pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, - ) => Promise; + ): Promise; /** * Previously, the master key was used for any additional key like the biometrics or pin key. * We have switched to using the user key for these purposes. This method is for clearing the state @@ -419,30 +430,36 @@ export abstract class CryptoService { * @param keySuffix The desired type of key to clear * @param userId The desired user */ - clearDeprecatedKeys: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.encrypt */ - encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey) => Promise; + abstract encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.encryptToBytes */ - encryptToBytes: (plainValue: Uint8Array, key?: SymmetricCryptoKey) => Promise; + abstract encryptToBytes( + plainValue: Uint8Array, + key?: SymmetricCryptoKey, + ): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.decryptToBytes */ - decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise; + abstract decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.decryptToUtf8 */ - decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise; + abstract decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.decryptToBytes */ - decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise; + abstract decryptFromBytes( + encBuffer: EncArrayBuffer, + key: SymmetricCryptoKey, + ): Promise; } diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index a5120e6898f1..9b4dde3676f6 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -7,23 +7,26 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; - abstract encryptToBytes: ( + abstract encryptToBytes( plainValue: Uint8Array, key?: SymmetricCryptoKey, - ) => Promise; - abstract decryptToUtf8: (encString: EncString, key: SymmetricCryptoKey) => Promise; - abstract decryptToBytes: (encThing: Encrypted, key: SymmetricCryptoKey) => Promise; - abstract rsaEncrypt: (data: Uint8Array, publicKey: Uint8Array) => Promise; - abstract rsaDecrypt: (data: EncString, privateKey: Uint8Array) => Promise; - abstract resolveLegacyKey: (key: SymmetricCryptoKey, encThing: Encrypted) => SymmetricCryptoKey; - abstract decryptItems: ( + ): Promise; + abstract decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise; + abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise; + abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; + abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; + abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; + abstract decryptItems( items: Decryptable[], key: SymmetricCryptoKey, - ) => Promise; + ): Promise; /** * Generates a base64-encoded hash of the given value * @param value The value to hash * @param algorithm The hashing algorithm to use */ - hash: (value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512") => Promise; + abstract hash( + value: string | Uint8Array, + algorithm: "sha1" | "sha256" | "sha512", + ): Promise; } diff --git a/libs/common/src/platform/abstractions/file-download/file-download.service.ts b/libs/common/src/platform/abstractions/file-download/file-download.service.ts index 44d082d72bfc..8bb70483eb72 100644 --- a/libs/common/src/platform/abstractions/file-download/file-download.service.ts +++ b/libs/common/src/platform/abstractions/file-download/file-download.service.ts @@ -1,5 +1,5 @@ import { FileDownloadRequest } from "./file-download.request"; export abstract class FileDownloadService { - download: (request: FileDownloadRequest) => void; + abstract download(request: FileDownloadRequest): void; } diff --git a/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts b/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts index e6a323817c9e..5f26a6662065 100644 --- a/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts +++ b/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts @@ -3,12 +3,12 @@ import { EncArrayBuffer } from "../../models/domain/enc-array-buffer"; import { EncString } from "../../models/domain/enc-string"; export abstract class FileUploadService { - upload: ( + abstract upload( uploadData: { url: string; fileUploadType: FileUploadType }, fileName: EncString, encryptedFileData: EncArrayBuffer, fileUploadMethods: FileUploadApiMethods, - ) => Promise; + ): Promise; } export type FileUploadApiMethods = { diff --git a/libs/common/src/platform/abstractions/i18n.service.ts b/libs/common/src/platform/abstractions/i18n.service.ts index 7b6eb9edc8a7..a1b44d956a98 100644 --- a/libs/common/src/platform/abstractions/i18n.service.ts +++ b/libs/common/src/platform/abstractions/i18n.service.ts @@ -3,8 +3,8 @@ import { Observable } from "rxjs"; import { TranslationService } from "./translation.service"; export abstract class I18nService extends TranslationService { - userSetLocale$: Observable; - locale$: Observable; + abstract userSetLocale$: Observable; + abstract locale$: Observable; abstract setLocale(locale: string): Promise; abstract init(): Promise; } diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts index a015182f89ff..223eb75038fe 100644 --- a/libs/common/src/platform/abstractions/key-generation.service.ts +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -11,7 +11,7 @@ export abstract class KeyGenerationService { * 512 bits = 64 bytes * @returns Generated key. */ - createKey: (bitLength: 256 | 512) => Promise; + abstract createKey(bitLength: 256 | 512): Promise; /** * Generates key material from CSPRNG and derives a 64 byte key from it. * Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869} @@ -22,11 +22,11 @@ export abstract class KeyGenerationService { * @param salt Optional. If not provided will be generated from CSPRNG. * @returns An object containing the salt, key material, and derived key. */ - createKeyWithPurpose: ( + abstract createKeyWithPurpose( bitLength: 128 | 192 | 256 | 512, purpose: string, salt?: string, - ) => Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }>; + ): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }>; /** * Derives a 64 byte key from key material. * @remark The key material should be generated from {@link createKey}, or {@link createKeyWithPurpose}. @@ -37,11 +37,11 @@ export abstract class KeyGenerationService { * Different purposes results in different keys, even with the same material. * @returns 64 byte derived key. */ - deriveKeyFromMaterial: ( + abstract deriveKeyFromMaterial( material: CsprngArray, salt: string, purpose: string, - ) => Promise; + ): Promise; /** * Derives a 32 byte key from a password using a key derivation function. * @param password Password to derive the key from. @@ -50,10 +50,10 @@ export abstract class KeyGenerationService { * @param kdfConfig Configuration for the key derivation function. * @returns 32 byte derived key. */ - deriveKeyFromPassword: ( + abstract deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, kdf: KdfType, kdfConfig: KdfConfig, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/platform/abstractions/log.service.ts b/libs/common/src/platform/abstractions/log.service.ts index 17db59768717..dffa3ca8d3e9 100644 --- a/libs/common/src/platform/abstractions/log.service.ts +++ b/libs/common/src/platform/abstractions/log.service.ts @@ -1,9 +1,9 @@ import { LogLevelType } from "../enums/log-level-type.enum"; export abstract class LogService { - debug: (message: string) => void; - info: (message: string) => void; - warning: (message: string) => void; - error: (message: string) => void; - write: (level: LogLevelType, message: string) => void; + abstract debug(message: string): void; + abstract info(message: string): void; + abstract warning(message: string): void; + abstract error(message: string): void; + abstract write(level: LogLevelType, message: string): void; } diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index 7c5f05f91982..ab4332c28392 100644 --- a/libs/common/src/platform/abstractions/messaging.service.ts +++ b/libs/common/src/platform/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ export abstract class MessagingService { - send: (subscriber: string, arg?: any) => void; + abstract send(subscriber: string, arg?: any): void; } diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index 0053b7d1d7c3..d518a17f7b4e 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -12,34 +12,34 @@ export type ClipboardOptions = { }; export abstract class PlatformUtilsService { - getDevice: () => DeviceType; - getDeviceString: () => string; - getClientType: () => ClientType; - isFirefox: () => boolean; - isChrome: () => boolean; - isEdge: () => boolean; - isOpera: () => boolean; - isVivaldi: () => boolean; - isSafari: () => boolean; - isMacAppStore: () => boolean; - isViewOpen: () => Promise; - launchUri: (uri: string, options?: any) => void; - getApplicationVersion: () => Promise; - getApplicationVersionNumber: () => Promise; - supportsWebAuthn: (win: Window) => boolean; - supportsDuo: () => boolean; - showToast: ( + abstract getDevice(): DeviceType; + abstract getDeviceString(): string; + abstract getClientType(): ClientType; + abstract isFirefox(): boolean; + abstract isChrome(): boolean; + abstract isEdge(): boolean; + abstract isOpera(): boolean; + abstract isVivaldi(): boolean; + abstract isSafari(): boolean; + abstract isMacAppStore(): boolean; + abstract isViewOpen(): Promise; + abstract launchUri(uri: string, options?: any): void; + abstract getApplicationVersion(): Promise; + abstract getApplicationVersionNumber(): Promise; + abstract supportsWebAuthn(win: Window): boolean; + abstract supportsDuo(): boolean; + abstract showToast( type: "error" | "success" | "warning" | "info", title: string, text: string | string[], options?: ToastOptions, - ) => void; - isDev: () => boolean; - isSelfHost: () => boolean; - copyToClipboard: (text: string, options?: ClipboardOptions) => void | boolean; - readFromClipboard: () => Promise; - supportsBiometric: () => Promise; - authenticateBiometric: () => Promise; - supportsSecureStorage: () => boolean; - getAutofillKeyboardShortcut: () => Promise; + ): void; + abstract isDev(): boolean; + abstract isSelfHost(): boolean; + abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; + abstract readFromClipboard(): Promise; + abstract supportsBiometric(): Promise; + abstract authenticateBiometric(): Promise; + abstract supportsSecureStorage(): boolean; + abstract getAutofillKeyboardShortcut(): Promise; } diff --git a/libs/common/src/platform/abstractions/system.service.ts b/libs/common/src/platform/abstractions/system.service.ts index 5a7e11f9a12d..204e336fbf4f 100644 --- a/libs/common/src/platform/abstractions/system.service.ts +++ b/libs/common/src/platform/abstractions/system.service.ts @@ -1,8 +1,8 @@ import { AuthService } from "../../auth/abstractions/auth.service"; export abstract class SystemService { - startProcessReload: (authService: AuthService) => Promise; - cancelProcessReload: () => void; - clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise; - clearPendingClipboard: () => Promise; + abstract startProcessReload(authService: AuthService): Promise; + abstract cancelProcessReload(): void; + abstract clearClipboard(clipboardValue: string, timeoutMs?: number): Promise; + abstract clearPendingClipboard(): Promise; } diff --git a/libs/common/src/platform/abstractions/translation.service.ts b/libs/common/src/platform/abstractions/translation.service.ts index 797965038a78..8a8faff1d8f2 100644 --- a/libs/common/src/platform/abstractions/translation.service.ts +++ b/libs/common/src/platform/abstractions/translation.service.ts @@ -1,8 +1,8 @@ export abstract class TranslationService { - supportedTranslationLocales: string[]; - translationLocale: string; - collator: Intl.Collator; - localeNames: Map; - t: (id: string, p1?: string | number, p2?: string | number, p3?: string | number) => string; - translate: (id: string, p1?: string, p2?: string, p3?: string) => string; + abstract supportedTranslationLocales: string[]; + abstract translationLocale: string; + abstract collator: Intl.Collator; + abstract localeNames: Map; + abstract t(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string; + abstract translate(id: string, p1?: string, p2?: string, p3?: string): string; } diff --git a/libs/common/src/platform/abstractions/validation.service.ts b/libs/common/src/platform/abstractions/validation.service.ts index c0985847bff7..b5aa71381a2d 100644 --- a/libs/common/src/platform/abstractions/validation.service.ts +++ b/libs/common/src/platform/abstractions/validation.service.ts @@ -1,3 +1,3 @@ export abstract class ValidationService { - showError: (data: any) => string[]; + abstract showError(data: any): string[]; } diff --git a/libs/common/src/platform/biometrics/biometric-state.service.ts b/libs/common/src/platform/biometrics/biometric-state.service.ts index 82c05542b4e5..20bba4971721 100644 --- a/libs/common/src/platform/biometrics/biometric-state.service.ts +++ b/libs/common/src/platform/biometrics/biometric-state.service.ts @@ -18,42 +18,42 @@ export abstract class BiometricStateService { /** * `true` if the currently active user has elected to store a biometric key to unlock their vault. */ - biometricUnlockEnabled$: Observable; // used to be biometricUnlock + abstract biometricUnlockEnabled$: Observable; // used to be biometricUnlock /** * If the user has elected to require a password on first unlock of an application instance, this key will store the * encrypted client key half used to unlock the vault. * * Tracks the currently active user */ - encryptedClientKeyHalf$: Observable; + abstract encryptedClientKeyHalf$: Observable; /** * whether or not a password is required on first unlock after opening the application * * tracks the currently active user */ - requirePasswordOnStart$: Observable; + abstract requirePasswordOnStart$: Observable; /** * Indicates the user has been warned about the security implications of using biometrics and, depending on the OS, * * tracks the currently active user. */ - dismissedRequirePasswordOnStartCallout$: Observable; + abstract dismissedRequirePasswordOnStartCallout$: Observable; /** * Whether the user has cancelled the biometric prompt. * * tracks the currently active user */ - promptCancelled$: Observable; + abstract promptCancelled$: Observable; /** * Whether the user has elected to automatically prompt for biometrics. * * tracks the currently active user */ - promptAutomatically$: Observable; + abstract promptAutomatically$: Observable; /** * Whether or not IPC fingerprint has been validated by the user this session. */ - fingerprintValidated$: Observable; + abstract fingerprintValidated$: Observable; /** * Updates the require password on start state for the currently active user. diff --git a/libs/common/src/platform/state/derived-state.provider.ts b/libs/common/src/platform/state/derived-state.provider.ts index cf0a0c56c770..218604824790 100644 --- a/libs/common/src/platform/state/derived-state.provider.ts +++ b/libs/common/src/platform/state/derived-state.provider.ts @@ -17,9 +17,9 @@ export abstract class DerivedStateProvider { * well as some memory persistent information. * @param dependencies The dependencies of the derive function */ - get: ( + abstract get( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, - ) => DerivedState; + ): DerivedState; } diff --git a/libs/common/src/platform/state/global-state.provider.ts b/libs/common/src/platform/state/global-state.provider.ts index 7c791b6b4d9d..5aa2b26a5b79 100644 --- a/libs/common/src/platform/state/global-state.provider.ts +++ b/libs/common/src/platform/state/global-state.provider.ts @@ -9,5 +9,5 @@ export abstract class GlobalStateProvider { * Gets a {@link GlobalState} scoped to the given {@link KeyDefinition} * @param keyDefinition - The {@link KeyDefinition} for which you want the state for. */ - get: (keyDefinition: KeyDefinition) => GlobalState; + abstract get(keyDefinition: KeyDefinition): GlobalState; } diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts index ddbb6a7c8755..a1e51552c73c 100644 --- a/libs/common/src/platform/state/state.provider.ts +++ b/libs/common/src/platform/state/state.provider.ts @@ -19,7 +19,7 @@ import { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.p */ export abstract class StateProvider { /** @see{@link ActiveUserStateProvider.activeUserId$} */ - activeUserId$: Observable; + abstract activeUserId$: Observable; /** * Gets a state observable for a given key and userId. @@ -149,10 +149,10 @@ export abstract class StateProvider { ): SingleUserState; /** @see{@link GlobalStateProvider.get} */ - getGlobal: (keyDefinition: KeyDefinition) => GlobalState; - getDerived: ( + abstract getGlobal(keyDefinition: KeyDefinition): GlobalState; + abstract getDerived( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, - ) => DerivedState; + ): DerivedState; } diff --git a/libs/common/src/platform/state/user-state.provider.ts b/libs/common/src/platform/state/user-state.provider.ts index 2f18f3678d8a..3af10218f875 100644 --- a/libs/common/src/platform/state/user-state.provider.ts +++ b/libs/common/src/platform/state/user-state.provider.ts @@ -39,7 +39,7 @@ export abstract class ActiveUserStateProvider { /** * Convenience re-emission of active user ID from {@link AccountService.activeAccount$} */ - activeUserId$: Observable; + abstract activeUserId$: Observable; /** * Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such diff --git a/libs/common/src/platform/theming/theme-state.service.ts b/libs/common/src/platform/theming/theme-state.service.ts index 42b5b1770cce..9c31733416b7 100644 --- a/libs/common/src/platform/theming/theme-state.service.ts +++ b/libs/common/src/platform/theming/theme-state.service.ts @@ -7,13 +7,13 @@ export abstract class ThemeStateService { /** * The users selected theme. */ - selectedTheme$: Observable; + abstract selectedTheme$: Observable; /** * A method for updating the current users configured theme. * @param theme The chosen user theme. */ - setSelectedTheme: (theme: ThemeType) => Promise; + abstract setSelectedTheme(theme: ThemeType): Promise; } const THEME_SELECTION = new KeyDefinition(THEMING_DISK, "selection", { From 0fbe64e5b95ebdc334be82ad430d1f6bdad96328 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:06:01 +0000 Subject: [PATCH 34/51] Autosync the updated translations (#8526) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/id/messages.json | 2 +- apps/browser/src/_locales/pt_PT/messages.json | 2 +- apps/browser/src/_locales/zh_CN/messages.json | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 1b3e7d4e36fb..44f6be8cef67 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -20,7 +20,7 @@ "message": "Masuk" }, "enterpriseSingleSignOn": { - "message": "Sistem Masuk Tunggal Perusahaan" + "message": "SSO Perusahaan" }, "cancel": { "message": "Batal" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a532ac50297f..117c5be6b4b5 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2913,7 +2913,7 @@ "message": "Mudar de conta" }, "switchAccounts": { - "message": "Mudar de contas" + "message": "Mudar de conta" }, "switchToAccount": { "message": "Mudar para conta" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fec2dcc2d993..1c269640c83c 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1500,7 +1500,7 @@ "message": "无效 PIN 码。" }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "无效的 PIN 输入尝试次数过多,正在退出登录。" + "message": "无效的 PIN 输入尝试次数过多,正在注销。" }, "unlockWithBiometrics": { "message": "使用生物识别解锁" @@ -1742,7 +1742,7 @@ "message": "Bitwarden 将不会询问是否为这些域名保存登录信息。您必须刷新页面才能使更改生效。" }, "excludedDomainsDescAlt": { - "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。您必须刷新页面才能使更改生效。" + "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。必须刷新页面才能使更改生效。" }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ 不是一个有效的域名", @@ -2314,7 +2314,7 @@ "message": "如何自动填充" }, "autofillSelectInfoWithCommand": { - "message": "从此界面选择一个项目,使用快捷方式 $COMMAND$,或探索设置中的其他选项。", + "message": "从此界面选择一个项目,使用快捷键 $COMMAND$,或探索设置中的其他选项。", "placeholders": { "command": { "content": "$1", @@ -2335,10 +2335,10 @@ "message": "自动填充键盘快捷键" }, "autofillShortcutNotSet": { - "message": "未设置自动填充快捷方式。请在浏览器设置中更改此设置。" + "message": "未设置自动填充快捷键。可在浏览器的设置中更改它。" }, "autofillShortcutText": { - "message": "自动填充快捷方式为: $COMMAND$。在浏览器设置中更改此项。", + "message": "自动填充快捷键为:$COMMAND$。可在浏览器的设置中更改它。", "placeholders": { "command": { "content": "$1", @@ -2928,7 +2928,7 @@ "message": "已达到账户上限。请注销一个账户后再添加其他账户。" }, "active": { - "message": "已生效" + "message": "活动的" }, "locked": { "message": "已锁定" @@ -2961,7 +2961,7 @@ } }, "commonImportFormats": { - "message": "通用格式", + "message": "常规格式", "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { @@ -2969,7 +2969,7 @@ "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "忽略此设置可能会导致 Bitwarden 自动填充菜单与浏览器自带功能产生冲突。", + "message": "忽略此选项可能会导致 Bitwarden 自动填充菜单与浏览器自带功能产生冲突。", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { From f30116b34dec0f2bd3dd3a5ce2d079f5eb888441 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:06:17 +0000 Subject: [PATCH 35/51] Autosync the updated translations (#8525) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/el/messages.json | 202 +++++++++---------- apps/desktop/src/locales/zh_CN/messages.json | 2 +- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 6d6fcaae4575..f5e18bdb85dd 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1087,7 +1087,7 @@ "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Ιδιόκτητες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey και το Duo." }, "premiumSignUpReports": { "message": "Ασφάλεια κωδικών, υγιής λογαριασμός και αναφορές παραβίασης δεδομένων για να διατηρήσετε ασφαλή τη λίστα σας." @@ -1399,7 +1399,7 @@ "message": "Μη έγκυρος κωδικός PIN." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Πάρα πολλές άκυρες απόπειρες εισαγωγής PIN. Γίνεται αποσύνδεση." }, "unlockWithWindowsHello": { "message": "Ξεκλειδώστε με το Windows Hello" @@ -1554,7 +1554,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Απαιτείται επαλήθευση", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1645,10 +1645,10 @@ "message": "Ενεργοποιήστε ένα πρόσθετο επίπεδο ασφάλειας απαιτώντας επικύρωση φράσης δακτυλικών αποτυπωμάτων κατά τη δημιουργία μιας σύνδεσης μεταξύ της επιφάνειας εργασίας σας και του προγράμματος περιήγησης. Όταν ενεργοποιηθεί, αυτό απαιτεί παρέμβαση χρήστη και επαλήθευση κάθε φορά που δημιουργείται σύνδεση." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Χρήση επιτάχυνσης υλικού" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Εξ ορισμού αυτή η ρύθμιση είναι ΕΝΕΡΓΗ. Απενεργοποιήστε μόνο αν αντιμετωπίζετε γραφικά προβλήματα. Απαιτείται επανεκκίνηση." }, "approve": { "message": "Έγκριση" @@ -1690,7 +1690,7 @@ "message": "Μια πολιτική του οργανισμού, επηρεάζει τις επιλογές ιδιοκτησίας σας." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Μια πολιτική οργανισμού έχει αποτρέψει την εισαγωγή στοιχείων στην προσωπική κρύπτη σας." }, "allSends": { "message": "Όλα τα Sends", @@ -1886,43 +1886,43 @@ "message": "Ο Κύριος Κωδικός Πρόσβασής σας άλλαξε πρόσφατα από διαχειριστή στον οργανισμό σας. Για να αποκτήσετε πρόσβαση στο vault, πρέπει να τον ενημερώσετε τώρα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Ο κύριος κωδικός πρόσβασης δεν πληροί τις απαιτήσεις πολιτικής αυτού του οργανισμού. Για να έχετε πρόσβαση στην κρύπτη, πρέπει να ενημερώσετε τον κύριο κωδικό σας άμεσα. Η διαδικασία θα σάς αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για το πολύ μία ώρα." }, "tryAgain": { - "message": "Try again" + "message": "Προσπαθήστε ξανά" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Απαιτείται επαλήθευση για αυτήν την ενέργεια. Ορίστε ένα PIN για να συνεχίσετε." }, "setPin": { - "message": "Set PIN" + "message": "Ορισμός PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Επαλήθευση με βιομετρικά" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Σε αναμονή επιβεβαίωσης" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Αδύνατη η ολοκλήρωση των βιομετρικών." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Χρειάζεστε μια διαφορετική μέθοδο;" }, "useMasterPassword": { - "message": "Use master password" + "message": "Χρήση κύριου κωδικού" }, "usePin": { - "message": "Use PIN" + "message": "Χρήση PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Χρήση βιομετρικών" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Εισάγετε τον κωδικό επαλήθευσης που έχει σταλεί στο email σας." }, "resendCode": { - "message": "Resend code" + "message": "Επαναποστολή κωδικού" }, "hours": { "message": "Ώρες" @@ -1944,7 +1944,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Οι πολιτικές του οργανισμού σας επηρεάζουν το χρονικό όριο λήξης της κρύ[της σας. Το μέγιστο επιτρεπόμενο χρονικό όριο λήξης vault είναι $HOURS$ ώρα(ες) και $MINUTES$ λεπτό(ά). H ενέργεια χρονικού ορίου λήξης της κρύπτης είναι ορισμένη ως $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -1961,7 +1961,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Οι πολιτικές του οργανισμού σας έχουν ορίσει την ενέργεια χρονικού ορίου λήξης κρύπτης σε $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2051,7 +2051,7 @@ "message": "Εξαγωγή Προσωπικού Vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Μόνο τα μεμονωμένα αντικείμενα κρύπτης που σχετίζονται με το $EMAIL$ θα εξαχθούν. Τα αντικείμενα κρύπτης οργανισμού δε θα συμπεριληφθούν. Μόνο πληροφορίες αντικειμένων κρύπτης θα εξαχθούν και δε θα περιλαμβάνουν συσχετιζόμενα συνημμένα.", "placeholders": { "email": { "content": "$1", @@ -2176,7 +2176,7 @@ "message": "Συνδεθείτε με άλλη συσκευή" }, "loginInitiated": { - "message": "Login initiated" + "message": "Η σύνδεση ξεκίνησε" }, "notificationSentDevice": { "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." @@ -2310,7 +2310,7 @@ } }, "windowsBiometricUpdateWarning": { - "message": "Bitwarden recommends updating your biometric settings to require your master password (or PIN) on the first unlock. Would you like to update your settings now?" + "message": "Το Bitwarden συστήνει την ενημέρωση των βιομετρικών ρυθμίσεών σας ώστε να απαιτηθεί ο κύριος κωδικός πρόσβασης (ή PIN) στο πρώτο ξεκλείδωμα. Θέλετε να ενημερώσετε τις ρυθμίσεις σας τώρα;" }, "windowsBiometricUpdateWarningTitle": { "message": "Ενημέρωση Προτεινόμενων Ρυθμίσεων" @@ -2319,74 +2319,74 @@ "message": "Απαιτείται έγκριση συσκευής. Επιλέξτε μια επιλογή έγκρισης παρακάτω:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Απομνημόνευση αυτής της συσκευής" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Αποεπιλογή αν γίνεται χρήση δημόσιας συσκευής" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Έγκριση από άλλη συσκευή σας" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Αίτηση έγκρισης διαχειριστή" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Έγκριση με τον κύριο κωδικό" }, "region": { - "message": "Region" + "message": "Περιοχή" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Απαιτείται αναγνωριστικό οργανισμού SSO." }, "eu": { - "message": "EU", + "message": "ΕΕ", "description": "European Union" }, "loggingInOn": { - "message": "Logging in on" + "message": "Σύνδεση σε" }, "selfHostedServer": { - "message": "self-hosted" + "message": "αυτο-φιλοξενούμενο" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "Δεν επιτρέπεται η πρόσβαση. Δεν έχετε άδεια για να δείτε αυτή τη σελίδα." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Επιτυχής δημιουργία λογαριασμού!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Ζητήθηκε έγκριση διαχειριστή" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Το αίτημά σας εστάλη στον διαχειριστή σας." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Θα ειδοποιηθείτε μόλις εγκριθεί." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Πρόβλημα σύνδεσης;" }, "loginApproved": { - "message": "Login approved" + "message": "Η σύνδεση εγκρίθηκε" }, "userEmailMissing": { - "message": "User email missing" + "message": "Το email του χρήστη λείπει" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Αξιόπιστη συσκευή" }, "inputRequired": { - "message": "Input is required." + "message": "Απαιτείται είσοδος." }, "required": { "message": "απαιτείται" }, "search": { - "message": "Search" + "message": "Αναζήτηση" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Η είσοδος πρέπει να είναι τουλάχιστον $COUNT$ χαρακτήρες.", "placeholders": { "count": { "content": "$1", @@ -2395,7 +2395,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Η είσοδος δεν πρέπει να υπερβαίνει τους $COUNT$ χαρακτήρες.", "placeholders": { "count": { "content": "$1", @@ -2404,7 +2404,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Οι ακόλουθοι χαρακτήρες δεν επιτρέπονται: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2413,7 +2413,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Η τιμή εισόδου πρέπει να είναι τουλάχιστον $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2422,7 +2422,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Η τιμή εισόδου δεν πρέπει να υπερβαίνει το $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2431,17 +2431,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 ή περισσότερα email δεν είναι έγκυρα" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Η είσοδος δεν πρέπει να περιέχει μόνο κενά.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Η είσοδος δεν είναι διεύθυνση email." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ πεδίο(α) παραπάνω χρειάζονται την προσοχή σας.", "placeholders": { "count": { "content": "$1", @@ -2450,22 +2450,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Επιλογή --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Πληκτρολογήστε για φιλτράρισμα --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Ανάκτηση επιλογών..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Δεν βρέθηκαν αντικείμενα" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Εκκαθάριση όλων" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ ακόμα", "placeholders": { "quantity": { "content": "$1", @@ -2474,44 +2474,44 @@ } }, "submenu": { - "message": "Submenu" + "message": "Υπομενού" }, "skipToContent": { - "message": "Skip to content" + "message": "Μετάβαση στο περιεχόμενο" }, "typePasskey": { - "message": "Passkey" + "message": "Κλειδί πρόσβασης" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Το κλειδί πρόσβασης δεν θα αντιγραφεί" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί στο κλωνοποιημένο στοιχείο. Θέλετε να συνεχίσετε την κλωνοποίηση αυτού του στοιχείου;" }, "aliasDomain": { - "message": "Alias domain" + "message": "Ψευδώνυμο τομέα" }, "importData": { - "message": "Import data", + "message": "Εισαγωγή δεδομένων", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "Import error" + "message": "Σφάλμα κατά την εισαγωγή" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Παρουσιάστηκε πρόβλημα με τα δεδομένα που επιχειρήσατε να εισαγάγετε. Παρακαλώ επιλύστε τα σφάλματα που αναφέρονται παρακάτω στο αρχείο πηγής και προσπαθήστε ξανά." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Επιλύστε τα παρακάτω σφάλματα και προσπαθήστε ξανά." }, "description": { - "message": "Description" + "message": "Περιγραφή" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Τα δεδομένα εισήχθησαν επιτυχώς" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Ένα σύνολο $AMOUNT$ στοιχείων εισήχθησαν.", "placeholders": { "amount": { "content": "$1", @@ -2520,10 +2520,10 @@ } }, "total": { - "message": "Total" + "message": "Σύνολο" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Εισαγάγετε δεδομένα στην $ORGANIZATION$. Τα δεδομένα σας μπορεί να μοιραστούν με μέλη αυτού του οργανισμού. Θέλετε να συνεχίσετε;", "placeholders": { "organization": { "content": "$1", @@ -2532,22 +2532,22 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Εκκινήστε το Duo και ακολουθήστε τα βήματα για να ολοκληρώσετε τη σύνδεση." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Η Σύνδεση δύο βημάτων Duo απαιτείται για τον λογαριασμό σας." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Εκκίνηση Duo στον περιηγητή" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Τα δεδομένα δεν έχουν διαμορφωθεί σωστά. Ελέγξτε το αρχείο εισαγωγής και δοκιμάστε ξανά." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Τίποτα δεν εισήχθη." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Σφάλμα αποκρυπτογράφησης του εξαγόμενου αρχείου. Το κλειδί κρυπτογράφησης δεν ταιριάζει με το κλειδί κρυπτογράφησης που χρησιμοποιήθηκε για την εξαγωγή των δεδομένων." }, "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." @@ -2633,68 +2633,68 @@ "message": "Multifactor authentication failed" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Συμπερίληψη κοινόχρηστων φακέλων" }, "lastPassEmail": { "message": "LastPass Email" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Εισαγωγή του λογαριασμού σας..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Απαιτείται πολυμερής ταυτοποίηση LastPass" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Εισαγάγετε τον κωδικό μιας χρήσης από την εφαρμογή επαλήθευσης" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Εγκρίνετε το αίτημα σύνδεσης στην εφαρμογή επαλήθευσης ή εισαγάγετε έναν κωδικό πρόσβασης μιας χρήσης." }, "passcode": { - "message": "Passcode" + "message": "Κωδικός" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "Κύριος κωδικός πρόσβασης LastPass" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Απαιτείται ταυτοποίηση LastPass" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Αναμονή ελέγχου ταυτότητας SSO" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Παρακαλούμε συνεχίστε τη σύνδεση χρησιμοποιώντας τα στοιχεία της εταιρείας σας." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Δείτε λεπτομερείς οδηγίες στην ιστοσελίδα βοήθειας μας στο", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Εισαγωγή απευθείας από το LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Εισαγωγή από CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Δοκιμάστε ξανά ή ψάξτε για ένα email από το LastPass για να επιβεβαιώσετε ότι είστε εσείς." }, "collection": { - "message": "Collection" + "message": "Συλλογή" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Εισαγάγετε το YubiKey που σχετίζεται με το λογαριασμό LastPass στη θύρα USB του υπολογιστή σας και στη συνέχεια αγγίξτε το κουμπί του." }, "commonImportFormats": { - "message": "Common formats", + "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Αντιμετώπιση Προβλημάτων" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Απενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Ενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 84ffcecbb84a..617e8dd9344a 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2685,7 +2685,7 @@ "message": "将与您的 LastPass 账户关联的 YubiKey 插入计算机的 USB 端口,然后触摸其按钮。" }, "commonImportFormats": { - "message": "通用格式", + "message": "常规格式", "description": "Label indicating the most common import formats" }, "troubleshooting": { From ddae908d86fe3126bf5b33cb4023a81551092de4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:06:25 +0000 Subject: [PATCH 36/51] Autosync the updated translations (#8524) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/nl/messages.json | 64 ++++++++++++------------ apps/web/src/locales/zh_CN/messages.json | 26 +++++----- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 0b99351080d6..ecdde73f68f9 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -579,7 +579,7 @@ "message": "Toegang" }, "accessLevel": { - "message": "Access level" + "message": "Toegangsniveau" }, "loggedOut": { "message": "Uitgelogd" @@ -660,13 +660,13 @@ "message": "Geef je passkey een naam om deze later te herkennen." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Gebruik voor kluis versleuteling" }, "useForVaultEncryptionInfo": { - "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." + "message": "Meld aan en ontgrendel op ondersteunde apparaten zonder uw hoofdwachtwoord. Volg de aanwijzingen in uw browser om dit te activeren." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Error reading passkey. Try again or uncheck this option." + "message": "Fout bij lezen van passkey. Probeer het opnieuw of selecteer een andere optie." }, "encryptionNotSupported": { "message": "Encryptie niet ondersteund" @@ -2036,7 +2036,7 @@ "message": "1 GB versleutelde opslag voor bijlagen." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Gepatenteerde 2 stap login opties zoals YubiKey en Duo." }, "premiumSignUpEmergency": { "message": "Noodtoegang" @@ -3621,7 +3621,7 @@ "message": "Encryptiesleutel bijwerken" }, "updateEncryptionSchemeDesc": { - "message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below." + "message": "Wij hebben de versleutelings- methode aangepast om betere beveiliging te kunnen leveren. Voer uw hoofdwachtwoord in om dit door te voeren." }, "updateEncryptionKeyWarning": { "message": "Na het bijwerken van je encryptiesleutel moet je je afmelden en weer aanmelden bij alle Bitwarden-applicaties die je gebruikt (zoals de mobiele app of browserextensies). Als je niet opnieuw inlogt (wat je nieuwe encryptiesleutel downloadt), kan dit gegevensbeschadiging tot gevolg hebben. We proberen je automatisch uit te loggen, maar het kan zijn dat dit met enige vertraging gebeurt." @@ -3781,7 +3781,7 @@ "message": "Dit item heeft oude bestandsbijlagen die aangepast moeten worden." }, "attachmentFixDescription": { - "message": "This attachment uses outdated encryption. Select 'Fix' to download, re-encrypt, and re-upload the attachment." + "message": "Deze bijlage maakt gebruik van verouderde versleuteling. Selecteer \"oplossen\" om het bestand te downloaden, opnieuw te versleutelen en vervolgens opnieuw te uploaden." }, "fix": { "message": "Oplossen", @@ -4044,10 +4044,10 @@ "message": "Je kunt dit tabblad nu sluiten en doorgaan in de extensie." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "U bent succesvol ingelogd" }, "thisWindowWillCloseIn5Seconds": { - "message": "This window will automatically close in 5 seconds" + "message": "Dit scherm sluit automatisch over 5 seconden" }, "includeAllTeamsFeatures": { "message": "Alle functionaliteit van Teams plus:" @@ -4776,13 +4776,13 @@ "message": "Accountherstel-administratie" }, "accountRecoveryPolicyDesc": { - "message": "Based on the encryption method, recover accounts when master passwords or trusted devices are forgotten or lost." + "message": "Gebaseerd op de huidige versleutelings- methode, herstel accounts wanneer hoofdwachtwoorden of vertrouwde apparaten vergeten of vermist zijn." }, "accountRecoveryPolicyWarning": { - "message": "Existing accounts with master passwords will require members to self-enroll before administrators can recover their accounts. Automatic enrollment will turn on account recovery for new members." + "message": "Bestaande accounts met hoofdwachtwoorden vereisen gebruikers om zelf in te schrijven voordat administratoren hun accounts kunnen herstellen. Automatische inschrijving zal automatisch account herstel inschakelen voor nieuwe gebruikers." }, "accountRecoverySingleOrgRequirementDesc": { - "message": "The single organization Enterprise policy must be turned on before activating this policy." + "message": "Het enkele organisatie beleid moet aangezet zijn voordat dit beleid geactiveerd kan worden." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische inschrijving" @@ -5114,7 +5114,7 @@ "message": "Automatisch invullen activeren" }, "activateAutofillPolicyDesc": { - "message": "Activate the auto-fill on page load setting on the browser extension for all existing and new members." + "message": "Activeer de automatisch invullen wanneer de pagina geladen is instelling in de browser extensie voor bestaande en nieuwe gebruikers." }, "experimentalFeature": { "message": "Gehackte of onbetrouwbare websites kunnen automatisch invullen bij laden van pagina misbruiken." @@ -5412,7 +5412,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'" }, "ssoPolicyHelpAnchor": { - "message": "require single sign-on authentication policy", + "message": "vereis single sign-on authenticatie beleid", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'" }, "ssoPolicyHelpEnd": { @@ -5429,15 +5429,15 @@ "message": "Key Connector" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The", + "message": "Verbind inloggen met SSO naar je zelf beheerde Decoderingsserver. Door deze optie te gebruiken hoeven gebruikers niet hun hoofdwachtwoord te gebruiken om kluis data te decoderen. Het", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { - "message": "require SSO authentication and single organization policies", + "message": "vereis het SSO authenticatie beleid en het enkele organisatie beleid", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescEnd": { - "message": "are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.", + "message": "is vereist om Key Connector decryptie in te stellen. Contacteer Bitwarden support voor assistentie.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "keyConnectorPolicyRestriction": { @@ -6197,7 +6197,7 @@ } }, "deleteServiceAccountToast": { - "message": "Service account deleted" + "message": "Service account verwijderd" }, "deleteServiceAccountsToast": { "message": "Serviceaccounts verwijderd" @@ -6734,7 +6734,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Gratis organisaties beschikken maximaal over $SEATCOUNT$ leden. Upgrade je abonnement om meer leden uit te kunnen nodigen.", "placeholders": { "seatcount": { "content": "$1", @@ -6881,7 +6881,7 @@ "message": "Werk je versleutelingsinstellingen bij om aan de nieuwe beveiligingsaanbevelingen te voldoen en de bescherming van je account te verbeteren." }, "changeKdfLoggedOutWarning": { - "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss." + "message": "Als u doorgaat zullen al uw actieve sessies word uitgelogd. U zult zich opnieuw aan moeten melden en 2 stappen verificatie moeten instellen. Wij raden u aan om uw gegevens eerst te exporteren voordat u uw encryptie instellingen aanpast om data verlies te voorkomen." }, "secretsManager": { "message": "Secrets Manager" @@ -7020,10 +7020,10 @@ "message": "Gelekt hoofdwachtwoord" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "Dit wachtwoord is gevonden in een datalek. Gebruik een uniek wachtwoord om je account te beveiligen. Weet je zeker dat je een gelekt wachtwoord wil gebruiken?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Zwak en gelekt hoofdwachtwoord" }, "weakAndBreachedMasterPasswordDesc": { "message": "Zwak wachtwoord geïdentificeerd en gevonden in een datalek. Gebruik een sterk en uniek wachtwoord om je account te beschermen. Weet je zeker dat je dit wachtwoord wilt gebruiken?" @@ -7271,7 +7271,7 @@ "message": "Volgende" }, "ssoLoginIsRequired": { - "message": "SSO login is required" + "message": "SSO login is vereist" }, "selectedRegionFlag": { "message": "Geselecteerde regionale vlag" @@ -7423,7 +7423,7 @@ "message": "Service account limiet (optioneel)" }, "maxServiceAccountCost": { - "message": "Max potential service account cost" + "message": "Maximale potentiële service account kosten" }, "loggedInExclamation": { "message": "Ingelogd!" @@ -7444,7 +7444,7 @@ "message": "Aliasdomein" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Heb je al een account?" }, "skipToContent": { "message": "Ga naar de inhoud" @@ -7471,7 +7471,7 @@ } }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Zie gedetailleerde instructies op onze hulp pagina hier", "description": "This is followed a by a hyperlink to the help website." }, "installBrowserExtension": { @@ -7487,13 +7487,13 @@ "message": "Er is een onverwachte fout opgetreden tijdens het laden van deze Send. Probeer het later opnieuw." }, "seatLimitReached": { - "message": "Seat limit has been reached" + "message": "Gebruikers limiet is bereikt" }, "contactYourProvider": { - "message": "Contact your provider to purchase additional seats." + "message": "Contacteer uw provider om aanvullende licenties aan te schaffen." }, "seatLimitReachedContactYourProvider": { - "message": "Seat limit has been reached. Contact your provider to purchase additional seats." + "message": "De limiet voor het aantal gebruikers is bereikt. Contacteer je provider om aanvullende licenties aan te schaffen." }, "collectionAccessRestricted": { "message": "Collectietoegang is beperkt" @@ -7511,7 +7511,7 @@ "message": "Toegang tot serviceaccount bijgewerkt" }, "commonImportFormats": { - "message": "Common formats", + "message": "Gangbare formaten", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { @@ -7601,10 +7601,10 @@ "message": "Providerportaal" }, "restrictedGroupAccess": { - "message": "You cannot add yourself to groups." + "message": "Het is niet mogelijk om jezelf toe te voegen aan groepen." }, "restrictedCollectionAccess": { - "message": "You cannot add yourself to collections." + "message": "Het is niet mogelijk om jezelf toe te voegen aan collecties." }, "assign": { "message": "Toewijzen" diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index f2d8619b32e3..e7668f85d59b 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7478,7 +7478,7 @@ "message": "安装浏览器扩展" }, "installBrowserExtensionDetails": { - "message": "使用扩展快速保存登录信息和自动填充表单,而无需打开网页应用程序。" + "message": "使用扩展快速保存登录信息和自动填充表单,而无需打开网页 App。" }, "projectAccessUpdated": { "message": "工程访问权限已更新" @@ -7511,7 +7511,7 @@ "message": "服务账户访问权限已更新" }, "commonImportFormats": { - "message": "通用格式", + "message": "常规格式", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { @@ -7533,7 +7533,7 @@ "description": "This describes new features and improvements for user roles and collections" }, "collectionEnhancementsLearnMore": { - "message": "了解更多关于集合管理" + "message": "了解更多关于集合管理的信息" }, "organizationInformation": { "message": "组织信息" @@ -7604,31 +7604,31 @@ "message": "您不能将自己添加到群组。" }, "restrictedCollectionAccess": { - "message": "您不能将自己添加到群组。" + "message": "您不能将自己添加到集合。" }, "assign": { - "message": "Assign" + "message": "分配" }, "assignToCollections": { - "message": "Assign to collections" + "message": "分配到集合" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "分配到这些集合" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "选择要共享项目的集合。当一个项目在某个集合中更新后,它将反映在所有集合中。只有能够访问这些集合的组织成员才能看到此项目。" }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "选择要分配的集合" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "没有分配任何集合" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "成功分配了集合" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "您选择了 $TOTAL_COUNT$ 个项目。其中的 $READONLY_COUNT$ 个项目由于您没有编辑权限,您将无法更新它们。", "placeholders": { "total_count": { "content": "$1", @@ -7641,6 +7641,6 @@ } }, "items": { - "message": "Items" + "message": "项目" } } From 37735436d1826f88e340e50d2fc048c211f0a846 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 28 Mar 2024 06:53:20 -0500 Subject: [PATCH 37/51] Move biometric texts all to getters (#8520) We cannot load biometric text on init because they are not valid everywhere. This was causing issues with settings storage on linux. --- .../src/app/accounts/settings.component.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 60aa2ebae847..a613328878d1 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -42,7 +42,6 @@ export class SettingsComponent implements OnInit { themeOptions: any[]; clearClipboardOptions: any[]; supportsBiometric: boolean; - additionalBiometricSettingsText: string; showAlwaysShowDock = false; requireEnableTray = false; showDuckDuckGoIntegrationOption = false; @@ -283,10 +282,6 @@ export class SettingsComponent implements OnInit { this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.additionalBiometricSettingsText = - this.biometricText === "unlockWithTouchId" - ? "additionalTouchIdSettings" - : "additionalWindowsHelloSettings"; this.previousVaultTimeout = this.form.value.vaultTimeout; this.refreshTimeoutSettings$ @@ -700,4 +695,15 @@ export class SettingsComponent implements OnInit { throw new Error("Unsupported platform"); } } + + get additionalBiometricSettingsText() { + switch (this.platformUtilsService.getDevice()) { + case DeviceType.MacOsDesktop: + return "additionalTouchIdSettings"; + case DeviceType.WindowsDesktop: + return "additionalWindowsHelloSettings"; + default: + throw new Error("Unsupported platform"); + } + } } From bd6b3266d43327183cb33c4df18005fc0436e9a1 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Thu, 28 Mar 2024 09:34:21 -0400 Subject: [PATCH 38/51] move auth request notification to service (#8451) - cleanup hanging promises --- .../popup/login-via-auth-request.component.ts | 7 +- .../login/login-via-auth-request.component.ts | 11 +-- .../login/login-via-auth-request.component.ts | 70 +------------------ .../login-via-auth-request.component.ts | 21 +++--- .../src/services/jslib-services.module.ts | 2 +- .../auth-request.service.abstraction.ts | 12 ++++ .../abstractions/login-strategy.service.ts | 9 --- .../auth-request/auth-request.service.ts | 12 ++++ .../login-strategy.service.ts | 14 ---- .../abstractions/anonymous-hub.service.ts | 4 +- .../auth/services/anonymous-hub.service.ts | 28 ++++---- 11 files changed, 57 insertions(+), 133 deletions(-) diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index a22636389a72..4ef1c78cb49d 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -1,5 +1,5 @@ import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { Router } from "@angular/router"; import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; @@ -28,10 +28,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv selector: "app-login-via-auth-request", templateUrl: "login-via-auth-request.component.html", }) -export class LoginViaAuthRequestComponent - extends BaseLoginWithDeviceComponent - implements OnInit, OnDestroy -{ +export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { constructor( router: Router, cryptoService: CryptoService, diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index b4242c36fba9..9a6fa8e38828 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -1,5 +1,5 @@ import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { Router } from "@angular/router"; import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; @@ -31,10 +31,7 @@ import { EnvironmentComponent } from "../environment.component"; selector: "app-login-via-auth-request", templateUrl: "login-via-auth-request.component.html", }) -export class LoginViaAuthRequestComponent - extends BaseLoginWithDeviceComponent - implements OnInit, OnDestroy -{ +export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { @ViewChild("environment", { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; showingModal = false; @@ -109,10 +106,6 @@ export class LoginViaAuthRequestComponent }); } - ngOnDestroy(): void { - super.ngOnDestroy(); - } - back() { this.location.back(); } diff --git a/apps/web/src/app/auth/login/login-via-auth-request.component.ts b/apps/web/src/app/auth/login/login-via-auth-request.component.ts index a3bf1160a3cd..5bca7183041e 100644 --- a/apps/web/src/app/auth/login/login-via-auth-request.component.ts +++ b/apps/web/src/app/auth/login/login-via-auth-request.component.ts @@ -1,75 +1,9 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; +import { Component } from "@angular/core"; import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, -} from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; - -import { StateService } from "../../core"; @Component({ selector: "app-login-via-auth-request", templateUrl: "login-via-auth-request.component.html", }) -export class LoginViaAuthRequestComponent - extends BaseLoginWithDeviceComponent - implements OnInit, OnDestroy -{ - constructor( - router: Router, - cryptoService: CryptoService, - cryptoFunctionService: CryptoFunctionService, - appIdService: AppIdService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - apiService: ApiService, - authService: AuthService, - logService: LogService, - environmentService: EnvironmentService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - anonymousHubService: AnonymousHubService, - validationService: ValidationService, - stateService: StateService, - loginService: LoginService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, - authRequestService: AuthRequestServiceAbstraction, - loginStrategyService: LoginStrategyServiceAbstraction, - ) { - super( - router, - cryptoService, - cryptoFunctionService, - appIdService, - passwordGenerationService, - apiService, - authService, - logService, - environmentService, - i18nService, - platformUtilsService, - anonymousHubService, - validationService, - stateService, - loginService, - deviceTrustCryptoService, - authRequestService, - loginStrategyService, - ); - } -} +export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {} diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index 45d7f563f7ae..b1d0b81922b8 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -68,7 +68,6 @@ export class LoginViaAuthRequestComponent private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; - // TODO: in future, go to child components and remove child constructors and let deps fall through to the super class constructor( protected router: Router, private cryptoService: CryptoService, @@ -98,14 +97,16 @@ export class LoginViaAuthRequestComponent this.email = this.loginService.getEmail(); } - //gets signalR push notification - this.loginStrategyService.authRequestPushNotification$ + // Gets signalR push notification + // Only fires on approval to prevent enumeration + this.authRequestService.authRequestPushNotification$ .pipe(takeUntil(this.destroy$)) .subscribe((id) => { - // Only fires on approval currently - // 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.verifyAndHandleApprovedAuthReq(id); + this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => { + this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.logService.error("Failed to use approved auth request: " + e.message); + }); }); } @@ -164,10 +165,10 @@ export class LoginViaAuthRequestComponent } } - ngOnDestroy(): void { + async ngOnDestroy() { + await this.anonymousHubService.stopHubConnection(); this.destroy$.next(); this.destroy$.complete(); - this.anonymousHubService.stopHubConnection(); } private async handleExistingAdminAuthRequest(adminAuthReqStorable: AdminAuthRequestStorable) { @@ -213,7 +214,7 @@ export class LoginViaAuthRequestComponent // Request still pending response from admin // So, create hub connection so that any approvals will be received via push notification - this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); + await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); } private async handleExistingAdminAuthReqDeletedOrDenied() { @@ -273,7 +274,7 @@ export class LoginViaAuthRequestComponent } if (reqResponse.id) { - this.anonymousHubService.createHubConnection(reqResponse.id); + await this.anonymousHubService.createHubConnection(reqResponse.id); } } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 521387181bd3..b2f5ee1f8986 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -866,7 +866,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: AnonymousHubServiceAbstraction, useClass: AnonymousHubService, - deps: [EnvironmentService, LoginStrategyServiceAbstraction, LogService], + deps: [EnvironmentService, AuthRequestServiceAbstraction], }), safeProvider({ provide: ValidationServiceAbstraction, diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index b91444d3e625..7af92fc8f8bb 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -1,7 +1,12 @@ +import { Observable } from "rxjs"; + import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export abstract class AuthRequestServiceAbstraction { + /** Emits an auth request id when an auth request has been approved. */ + authRequestPushNotification$: Observable; /** * Approve or deny an auth request. * @param approve True to approve, false to deny. @@ -54,4 +59,11 @@ export abstract class AuthRequestServiceAbstraction { pubKeyEncryptedMasterKeyHash: string, privateKey: ArrayBuffer, ) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>; + + /** + * Handles incoming auth request push notifications. + * @param notification push notification. + * @remark We should only be receiving approved push notifications to prevent enumeration. + */ + abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void; } diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index e3ed63c73749..eae6dc2a275f 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -4,7 +4,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; -import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { MasterKey } from "@bitwarden/common/types/key"; import { @@ -21,10 +20,6 @@ export abstract class LoginStrategyServiceAbstraction { * Emits null if the session has timed out. */ currentAuthType$: Observable; - /** - * Emits when an auth request has been approved. - */ - authRequestPushNotification$: Observable; /** * If the login strategy uses the email address of the user, this * will return it. Otherwise, it will return null. @@ -77,10 +72,6 @@ export abstract class LoginStrategyServiceAbstraction { * Creates a master key from the provided master password and email. */ makePreloginKey: (masterPassword: string, email: string) => Promise; - /** - * Sends a notification to {@link LoginStrategyServiceAbstraction.authRequestPushNotification} - */ - sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => Promise; /** * Sends a response to an auth request. */ diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index ab33780fe6c5..ff33eadfba76 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -1,6 +1,9 @@ +import { Observable, Subject } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -11,6 +14,9 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; export class AuthRequestService implements AuthRequestServiceAbstraction { + private authRequestPushNotificationSubject = new Subject(); + authRequestPushNotification$: Observable; + constructor( private appIdService: AppIdService, private cryptoService: CryptoService, @@ -126,4 +132,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { masterKeyHash, }; } + + sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void { + if (notification.id != null) { + this.authRequestPushNotificationSubject.next(notification.id); + } + } } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 428258308aa2..b55f38af7f69 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -1,7 +1,6 @@ import { combineLatestWith, distinctUntilChanged, - filter, firstValueFrom, map, Observable, @@ -23,7 +22,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -81,8 +79,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { >; currentAuthType$: Observable; - // TODO: move to auth request service - authRequestPushNotification$: Observable; constructor( protected cryptoService: CryptoService, @@ -114,9 +110,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); this.currentAuthType$ = this.currentAuthnTypeState.state$; - this.authRequestPushNotification$ = this.authRequestPushNotificationState.state$.pipe( - filter((id) => id != null), - ); this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe( distinctUntilChanged(), combineLatestWith(this.loginStrategyCacheState.state$), @@ -256,13 +249,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); } - // TODO move to auth request service - async sendAuthRequestPushNotification(notification: AuthRequestPushNotification): Promise { - if (notification.id != null) { - await this.authRequestPushNotificationState.update((_) => notification.id); - } - } - // TODO: move to auth request service async passwordlessLogin( id: string, diff --git a/libs/common/src/auth/abstractions/anonymous-hub.service.ts b/libs/common/src/auth/abstractions/anonymous-hub.service.ts index 43bdabd512c9..e108dccbb60d 100644 --- a/libs/common/src/auth/abstractions/anonymous-hub.service.ts +++ b/libs/common/src/auth/abstractions/anonymous-hub.service.ts @@ -1,4 +1,4 @@ export abstract class AnonymousHubService { - createHubConnection: (token: string) => void; - stopHubConnection: () => void; + createHubConnection: (token: string) => Promise; + stopHubConnection: () => Promise; } diff --git a/libs/common/src/auth/services/anonymous-hub.service.ts b/libs/common/src/auth/services/anonymous-hub.service.ts index fe8ae641832c..747fbc39178e 100644 --- a/libs/common/src/auth/services/anonymous-hub.service.ts +++ b/libs/common/src/auth/services/anonymous-hub.service.ts @@ -7,13 +7,13 @@ import { import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; import { firstValueFrom } from "rxjs"; -import { LoginStrategyServiceAbstraction } from "../../../../auth/src/common/abstractions/login-strategy.service"; +import { AuthRequestServiceAbstraction } from "../../../../auth/src/common/abstractions"; +import { NotificationType } from "../../enums"; import { AuthRequestPushNotification, NotificationResponse, } from "../../models/response/notification.response"; import { EnvironmentService } from "../../platform/abstractions/environment.service"; -import { LogService } from "../../platform/abstractions/log.service"; import { AnonymousHubService as AnonymousHubServiceAbstraction } from "../abstractions/anonymous-hub.service"; export class AnonymousHubService implements AnonymousHubServiceAbstraction { @@ -22,8 +22,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction { constructor( private environmentService: EnvironmentService, - private loginStrategyService: LoginStrategyServiceAbstraction, - private logService: LogService, + private authRequestService: AuthRequestServiceAbstraction, ) {} async createHubConnection(token: string) { @@ -37,26 +36,25 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction { .withHubProtocol(new MessagePackHubProtocol() as IHubProtocol) .build(); - this.anonHubConnection.start().catch((error) => this.logService.error(error)); + await this.anonHubConnection.start(); this.anonHubConnection.on("AuthRequestResponseRecieved", (data: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises this.ProcessNotification(new NotificationResponse(data)); }); } - stopHubConnection() { + async stopHubConnection() { if (this.anonHubConnection) { - // 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.anonHubConnection.stop(); + await this.anonHubConnection.stop(); } } - private async ProcessNotification(notification: NotificationResponse) { - await this.loginStrategyService.sendAuthRequestPushNotification( - notification.payload as AuthRequestPushNotification, - ); + private ProcessNotification(notification: NotificationResponse) { + switch (notification.type) { + case NotificationType.AuthRequestResponse: + this.authRequestService.sendAuthRequestPushNotification( + notification.payload as AuthRequestPushNotification, + ); + } } } From 65353ae71d0a5d1051d3a5da7bcccefbb8c431af Mon Sep 17 00:00:00 2001 From: Victoria League Date: Thu, 28 Mar 2024 10:26:26 -0400 Subject: [PATCH 39/51] [CL-215] Fix broken icon stories and clarify usage (#8484) --- libs/components/src/icon/icon.stories.ts | 27 +++++++++--------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 95bf457517d7..54cdd7928cd2 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -1,31 +1,24 @@ import { Meta, StoryObj } from "@storybook/angular"; import { BitIconComponent } from "./icon.component"; +import * as GenericIcons from "./icons"; export default { title: "Component Library/Icon", component: BitIconComponent, - args: { - icon: "reportExposedPasswords", - }, } as Meta; type Story = StoryObj; -export const ReportExposedPasswords: Story = { - render: (args) => ({ - props: args, - template: ` -
- -
- `, - }), -}; - -export const UnknownIcon: Story = { - ...ReportExposedPasswords, +export const Default: Story = { args: { - icon: "unknown" as any, + icon: GenericIcons.NoAccess, + }, + argTypes: { + icon: { + options: Object.keys(GenericIcons), + mapping: GenericIcons, + control: { type: "select" }, + }, }, }; From df058ba399a5f9f12dcbcb3a8e41eba9b1e72bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 28 Mar 2024 12:19:12 -0400 Subject: [PATCH 40/51] [PM-6146] generator history (#8497) * introduce `GeneratorHistoryService` abstraction * implement generator history service with `LocalGeneratorHistoryService` * cache decrypted data using `ReplaySubject` instead of `DerivedState` * move Jsonification from `DataPacker` to `SecretClassifier` because the classifier is the only component that has full type information. The data packer still handles stringification. --- .../generator-history.abstraction.ts | 47 ++++ .../history/generated-credential.spec.ts | 58 ++++ .../generator/history/generated-credential.ts | 47 ++++ .../src/tools/generator/history/index.ts | 2 + .../local-generator-history.service.spec.ts | 198 ++++++++++++++ .../local-generator-history.service.ts | 116 ++++++++ .../src/tools/generator/history/options.ts | 10 + .../tools/generator/key-definition.spec.ts | 9 - .../src/tools/generator/key-definitions.ts | 11 +- .../generator/state/classified-format.ts | 19 ++ .../state/data-packer.abstraction.ts | 2 +- .../state/padded-data-packer.spec.ts | 10 - .../generator/state/padded-data-packer.ts | 2 +- .../generator/state/secret-classifier.spec.ts | 18 +- .../generator/state/secret-classifier.ts | 22 +- .../state/secret-key-definition.spec.ts | 22 ++ .../generator/state/secret-key-definition.ts | 17 +- .../generator/state/secret-state.spec.ts | 20 +- .../src/tools/generator/state/secret-state.ts | 253 ++++++++---------- .../state/user-encryptor.abstraction.ts | 6 +- .../state/user-key-encryptor.spec.ts | 8 +- .../generator/state/user-key-encryptor.ts | 6 +- 22 files changed, 691 insertions(+), 212 deletions(-) create mode 100644 libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts create mode 100644 libs/common/src/tools/generator/history/generated-credential.spec.ts create mode 100644 libs/common/src/tools/generator/history/generated-credential.ts create mode 100644 libs/common/src/tools/generator/history/index.ts create mode 100644 libs/common/src/tools/generator/history/local-generator-history.service.spec.ts create mode 100644 libs/common/src/tools/generator/history/local-generator-history.service.ts create mode 100644 libs/common/src/tools/generator/history/options.ts create mode 100644 libs/common/src/tools/generator/state/classified-format.ts diff --git a/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts new file mode 100644 index 000000000000..edda0dcb2bab --- /dev/null +++ b/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts @@ -0,0 +1,47 @@ +import { Observable } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { GeneratedCredential, GeneratorCategory } from "../history"; + +/** Tracks the history of password generations. + * Each user gets their own store. + */ +export abstract class GeneratorHistoryService { + /** Tracks a new credential. When an item with the same `credential` value + * is found, this method does nothing. When the total number of items exceeds + * {@link HistoryServiceOptions.maxTotal}, then the oldest items exceeding the total + * are deleted. + * @param userId identifies the user storing the credential. + * @param credential stored by the history service. + * @param date when the credential was generated. If this is omitted, then the generator + * uses the date the credential was added to the store instead. + * @returns a promise that completes with the added credential. If the credential + * wasn't added, then the promise completes with `null`. + * @remarks this service is not suitable for use with vault items/ciphers. It models only + * a history of an individually generated credential, while a vault item's history + * may contain several credentials that are better modelled as atomic versions of the + * vault item itself. + */ + track: ( + userId: UserId, + credential: string, + category: GeneratorCategory, + date?: Date, + ) => Promise; + + /** Removes a matching credential from the history service. + * @param userId identifies the user taking the credential. + * @param credential to match in the history service. + * @returns A promise that completes with the credential read. If the credential wasn't found, + * the promise completes with null. + * @remarks this can be used to extract an entry when a credential is stored in the vault. + */ + take: (userId: UserId, credential: string) => Promise; + + /** Lists all credentials for a user. + * @param userId identifies the user listing the credential. + * @remarks This field is eventually consistent with `track` and `take` operations. + * It is not guaranteed to immediately reflect those changes. + */ + credentials$: (userId: UserId) => Observable; +} diff --git a/libs/common/src/tools/generator/history/generated-credential.spec.ts b/libs/common/src/tools/generator/history/generated-credential.spec.ts new file mode 100644 index 000000000000..170030bad172 --- /dev/null +++ b/libs/common/src/tools/generator/history/generated-credential.spec.ts @@ -0,0 +1,58 @@ +import { GeneratorCategory, GeneratedCredential } from "./"; + +describe("GeneratedCredential", () => { + describe("constructor", () => { + it("assigns credential", () => { + const result = new GeneratedCredential("example", "passphrase", new Date(100)); + + expect(result.credential).toEqual("example"); + }); + + it("assigns category", () => { + const result = new GeneratedCredential("example", "passphrase", new Date(100)); + + expect(result.category).toEqual("passphrase"); + }); + + it("passes through date parameters", () => { + const result = new GeneratedCredential("example", "password", new Date(100)); + + expect(result.generationDate).toEqual(new Date(100)); + }); + + it("converts numeric dates to Dates", () => { + const result = new GeneratedCredential("example", "password", 100); + + expect(result.generationDate).toEqual(new Date(100)); + }); + }); + + it("toJSON converts from a credential into a JSON object", () => { + const credential = new GeneratedCredential("example", "password", new Date(100)); + + const result = credential.toJSON(); + + expect(result).toEqual({ + credential: "example", + category: "password" as GeneratorCategory, + generationDate: 100, + }); + }); + + it("fromJSON converts Json objects into credentials", () => { + const jsonValue = { + credential: "example", + category: "password" as GeneratorCategory, + generationDate: 100, + }; + + const result = GeneratedCredential.fromJSON(jsonValue); + + expect(result).toBeInstanceOf(GeneratedCredential); + expect(result).toEqual({ + credential: "example", + category: "password", + generationDate: new Date(100), + }); + }); +}); diff --git a/libs/common/src/tools/generator/history/generated-credential.ts b/libs/common/src/tools/generator/history/generated-credential.ts new file mode 100644 index 000000000000..59a9623bf7e4 --- /dev/null +++ b/libs/common/src/tools/generator/history/generated-credential.ts @@ -0,0 +1,47 @@ +import { Jsonify } from "type-fest"; + +import { GeneratorCategory } from "./options"; + +/** A credential generation result */ +export class GeneratedCredential { + /** + * Instantiates a generated credential + * @param credential The value of the generated credential (e.g. a password) + * @param category The kind of credential + * @param generationDate The date that the credential was generated. + * Numeric values should are interpreted using {@link Date.valueOf} + * semantics. + */ + constructor( + readonly credential: string, + readonly category: GeneratorCategory, + generationDate: Date | number, + ) { + if (typeof generationDate === "number") { + this.generationDate = new Date(generationDate); + } else { + this.generationDate = generationDate; + } + } + + /** The date that the credential was generated */ + generationDate: Date; + + /** Constructs a credential from its `toJSON` representation */ + static fromJSON(jsonValue: Jsonify) { + return new GeneratedCredential( + jsonValue.credential, + jsonValue.category, + jsonValue.generationDate, + ); + } + + /** Serializes a credential to a JSON-compatible object */ + toJSON() { + return { + credential: this.credential, + category: this.category, + generationDate: this.generationDate.valueOf(), + }; + } +} diff --git a/libs/common/src/tools/generator/history/index.ts b/libs/common/src/tools/generator/history/index.ts new file mode 100644 index 000000000000..1952a849af2f --- /dev/null +++ b/libs/common/src/tools/generator/history/index.ts @@ -0,0 +1,2 @@ +export { GeneratorCategory } from "./options"; +export { GeneratedCredential } from "./generated-credential"; diff --git a/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts b/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts new file mode 100644 index 000000000000..57dde51fc130 --- /dev/null +++ b/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts @@ -0,0 +1,198 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../spec"; +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncString } from "../../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../../types/csprng"; +import { UserId } from "../../../types/guid"; +import { UserKey } from "../../../types/key"; + +import { LocalGeneratorHistoryService } from "./local-generator-history.service"; + +const SomeUser = "SomeUser" as UserId; +const AnotherUser = "AnotherUser" as UserId; + +describe("LocalGeneratorHistoryService", () => { + const encryptService = mock(); + const keyService = mock(); + const userKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey; + + beforeEach(() => { + encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString)); + encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString)); + keyService.getUserKey.mockImplementation(() => Promise.resolve(userKey)); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("credential$", () => { + it("returns an empty list when no credentials are stored", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + const result = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toEqual([]); + }); + }); + + describe("track", () => { + it("stores a password", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "password"); + await awaitAsync(); + const [result] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toMatchObject({ credential: "example", category: "password" }); + }); + + it("stores a passphrase", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "passphrase"); + await awaitAsync(); + const [result] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toMatchObject({ credential: "example", category: "passphrase" }); + }); + + it("stores a specific date when one is provided", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "password", new Date(100)); + await awaitAsync(); + const [result] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toEqual({ + credential: "example", + category: "password", + generationDate: new Date(100), + }); + }); + + it("skips storing a credential when it's already stored (ignores category)", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", "passphrase"); + await awaitAsync(); + const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(firstResult).toMatchObject({ credential: "example", category: "password" }); + expect(secondResult).toBeUndefined(); + }); + + it("stores multiple credentials when the credential value is different", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "secondResult", "password"); + await history.track(SomeUser, "firstResult", "password"); + await awaitAsync(); + const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(firstResult).toMatchObject({ credential: "firstResult", category: "password" }); + expect(secondResult).toMatchObject({ credential: "secondResult", category: "password" }); + }); + + it("removes history items exceeding maxTotal configuration", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider, { + maxTotal: 1, + }); + + await history.track(SomeUser, "removed result", "password"); + await history.track(SomeUser, "example", "password"); + await awaitAsync(); + const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(firstResult).toMatchObject({ credential: "example", category: "password" }); + expect(secondResult).toBeUndefined(); + }); + + it("stores history items in per-user collections", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider, { + maxTotal: 1, + }); + + await history.track(SomeUser, "some user example", "password"); + await history.track(AnotherUser, "another user example", "password"); + await awaitAsync(); + const [someFirstResult, someSecondResult] = await firstValueFrom( + history.credentials$(SomeUser), + ); + const [anotherFirstResult, anotherSecondResult] = await firstValueFrom( + history.credentials$(AnotherUser), + ); + + expect(someFirstResult).toMatchObject({ + credential: "some user example", + category: "password", + }); + expect(someSecondResult).toBeUndefined(); + expect(anotherFirstResult).toMatchObject({ + credential: "another user example", + category: "password", + }); + expect(anotherSecondResult).toBeUndefined(); + }); + }); + + describe("take", () => { + it("returns null when there are no credentials stored", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + const result = await history.take(SomeUser, "example"); + + expect(result).toBeNull(); + }); + + it("returns null when the credential wasn't found", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + await history.track(SomeUser, "example", "password"); + + const result = await history.take(SomeUser, "not found"); + + expect(result).toBeNull(); + }); + + it("returns a matching credential", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + await history.track(SomeUser, "example", "password"); + + const result = await history.take(SomeUser, "example"); + + expect(result).toMatchObject({ + credential: "example", + category: "password", + }); + }); + + it("removes a matching credential", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + await history.track(SomeUser, "example", "password"); + + await history.take(SomeUser, "example"); + await awaitAsync(); + const results = await firstValueFrom(history.credentials$(SomeUser)); + + expect(results).toEqual([]); + }); + }); +}); diff --git a/libs/common/src/tools/generator/history/local-generator-history.service.ts b/libs/common/src/tools/generator/history/local-generator-history.service.ts new file mode 100644 index 000000000000..3a65890c50d6 --- /dev/null +++ b/libs/common/src/tools/generator/history/local-generator-history.service.ts @@ -0,0 +1,116 @@ +import { map } from "rxjs"; + +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { SingleUserState, StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { GeneratorHistoryService } from "../abstractions/generator-history.abstraction"; +import { GENERATOR_HISTORY } from "../key-definitions"; +import { PaddedDataPacker } from "../state/padded-data-packer"; +import { SecretState } from "../state/secret-state"; +import { UserKeyEncryptor } from "../state/user-key-encryptor"; + +import { GeneratedCredential } from "./generated-credential"; +import { GeneratorCategory, HistoryServiceOptions } from "./options"; + +const OPTIONS_FRAME_SIZE = 2048; + +/** Tracks the history of password generations local to a device. + * {@link GeneratorHistoryService} + */ +export class LocalGeneratorHistoryService extends GeneratorHistoryService { + constructor( + private readonly encryptService: EncryptService, + private readonly keyService: CryptoService, + private readonly stateProvider: StateProvider, + private readonly options: HistoryServiceOptions = { maxTotal: 100 }, + ) { + super(); + } + + private _credentialStates = new Map>(); + + /** {@link GeneratorHistoryService.track} */ + track = async (userId: UserId, credential: string, category: GeneratorCategory, date?: Date) => { + const state = this.getCredentialState(userId); + let result: GeneratedCredential = null; + + await state.update( + (credentials) => { + credentials = credentials ?? []; + + // add the result + result = new GeneratedCredential(credential, category, date ?? Date.now()); + credentials.unshift(result); + + // trim history + const removeAt = Math.max(0, this.options.maxTotal); + credentials.splice(removeAt, Infinity); + + return credentials; + }, + { + shouldUpdate: (credentials) => + credentials?.some((f) => f.credential !== credential) ?? true, + }, + ); + + return result; + }; + + /** {@link GeneratorHistoryService.take} */ + take = async (userId: UserId, credential: string) => { + const state = this.getCredentialState(userId); + let credentialIndex: number; + let result: GeneratedCredential = null; + + await state.update( + (credentials) => { + credentials = credentials ?? []; + + [result] = credentials.splice(credentialIndex, 1); + return credentials; + }, + { + shouldUpdate: (credentials) => { + credentialIndex = credentials?.findIndex((f) => f.credential === credential) ?? -1; + return credentialIndex >= 0; + }, + }, + ); + + return result; + }; + + /** {@link GeneratorHistoryService.credentials$} */ + credentials$ = (userId: UserId) => { + return this.getCredentialState(userId).state$.pipe(map((credentials) => credentials ?? [])); + }; + + private getCredentialState(userId: UserId) { + let state = this._credentialStates.get(userId); + + if (!state) { + state = this.createSecretState(userId); + this._credentialStates.set(userId, state); + } + + return state; + } + + private createSecretState(userId: UserId) { + // construct the encryptor + const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE); + const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer); + + const state = SecretState.from< + GeneratedCredential[], + number, + GeneratedCredential, + Record, + GeneratedCredential + >(userId, GENERATOR_HISTORY, this.stateProvider, encryptor); + + return state; + } +} diff --git a/libs/common/src/tools/generator/history/options.ts b/libs/common/src/tools/generator/history/options.ts new file mode 100644 index 000000000000..53716ec33abc --- /dev/null +++ b/libs/common/src/tools/generator/history/options.ts @@ -0,0 +1,10 @@ +/** Kinds of credentials that can be stored by the history service */ +export type GeneratorCategory = "password" | "passphrase"; + +/** Configuration options for the history service */ +export type HistoryServiceOptions = { + /** Total number of records retained across all types. + * @remarks Setting this to 0 or less disables history completely. + * */ + maxTotal: number; +}; diff --git a/libs/common/src/tools/generator/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts index 735377a5ba2a..f21767e77e89 100644 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ b/libs/common/src/tools/generator/key-definition.spec.ts @@ -1,5 +1,4 @@ import { - ENCRYPTED_HISTORY, EFF_USERNAME_SETTINGS, CATCHALL_SETTINGS, SUBADDRESS_SETTINGS, @@ -101,12 +100,4 @@ describe("Key definitions", () => { expect(result).toBe(value); }); }); - - describe("ENCRYPTED_HISTORY", () => { - it("should pass through deserialization", () => { - const value = {}; - const result = ENCRYPTED_HISTORY.deserializer(value as any); - expect(result).toBe(value); - }); - }); }); diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index bb7c4e8a0861..d51af70f2e22 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,8 +1,10 @@ import { GENERATOR_DISK, KeyDefinition } from "../../platform/state"; +import { GeneratedCredential } from "./history/generated-credential"; import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options"; -import { GeneratedPasswordHistory } from "./password/generated-password-history"; import { PasswordGenerationOptions } from "./password/password-generation-options"; +import { SecretClassifier } from "./state/secret-classifier"; +import { SecretKeyDefinition } from "./state/secret-key-definition"; import { CatchallGenerationOptions } from "./username/catchall-generator-options"; import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; import { @@ -107,10 +109,11 @@ export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition( ); /** encrypted password generation history */ -export const ENCRYPTED_HISTORY = new KeyDefinition( +export const GENERATOR_HISTORY = SecretKeyDefinition.array( GENERATOR_DISK, - "passwordGeneratorHistory", + "localGeneratorHistory", + SecretClassifier.allSecret(), { - deserializer: (value) => value, + deserializer: GeneratedCredential.fromJSON, }, ); diff --git a/libs/common/src/tools/generator/state/classified-format.ts b/libs/common/src/tools/generator/state/classified-format.ts new file mode 100644 index 000000000000..93147a0fb53f --- /dev/null +++ b/libs/common/src/tools/generator/state/classified-format.ts @@ -0,0 +1,19 @@ +import { Jsonify } from "type-fest"; + +/** Describes the structure of data stored by the SecretState's + * encrypted state. Notably, this interface ensures that `Disclosed` + * round trips through JSON serialization. It also preserves the + * Id. + */ +export type ClassifiedFormat = { + /** Identifies records. `null` when storing a `value` */ + readonly id: Id | null; + /** Serialized {@link EncString} of the secret state's + * secret-level classified data. + */ + readonly secret: string; + /** serialized representation of the secret state's + * disclosed-level classified data. + */ + readonly disclosed: Jsonify; +}; diff --git a/libs/common/src/tools/generator/state/data-packer.abstraction.ts b/libs/common/src/tools/generator/state/data-packer.abstraction.ts index cb712e0fd9bc..439fbb66c8c5 100644 --- a/libs/common/src/tools/generator/state/data-packer.abstraction.ts +++ b/libs/common/src/tools/generator/state/data-packer.abstraction.ts @@ -9,7 +9,7 @@ export abstract class DataPacker { * @param value is packed into the string * @returns the packed string */ - abstract pack(value: Data): string; + abstract pack(value: Jsonify): string; /** Unpacks a string produced by pack. * @param packedValue is the string to unpack diff --git a/libs/common/src/tools/generator/state/padded-data-packer.spec.ts b/libs/common/src/tools/generator/state/padded-data-packer.spec.ts index 3cf225026b4b..7e1d506988a1 100644 --- a/libs/common/src/tools/generator/state/padded-data-packer.spec.ts +++ b/libs/common/src/tools/generator/state/padded-data-packer.spec.ts @@ -88,14 +88,4 @@ describe("UserKeyEncryptor", () => { expect(unpacked).toEqual(input); }); - - it("should unpack a packed JSON-serializable value", () => { - const dataPacker = new PaddedDataPacker(8); - const input = { foo: new Date(100) }; - - const packed = dataPacker.pack(input); - const unpacked = dataPacker.unpack(packed); - - expect(unpacked).toEqual({ foo: "1970-01-01T00:00:00.100Z" }); - }); }); diff --git a/libs/common/src/tools/generator/state/padded-data-packer.ts b/libs/common/src/tools/generator/state/padded-data-packer.ts index b55dfa378b74..e2f5058b2178 100644 --- a/libs/common/src/tools/generator/state/padded-data-packer.ts +++ b/libs/common/src/tools/generator/state/padded-data-packer.ts @@ -37,7 +37,7 @@ export class PaddedDataPacker extends DataPackerAbstraction { * with the frameSize. * @see {@link DataPackerAbstraction.unpack} */ - pack(value: Secret) { + pack(value: Jsonify) { // encode the value const json = JSON.stringify(value); const b64 = Utils.fromUtf8ToB64(json); diff --git a/libs/common/src/tools/generator/state/secret-classifier.spec.ts b/libs/common/src/tools/generator/state/secret-classifier.spec.ts index 819cd1092331..41dd8dc71bf9 100644 --- a/libs/common/src/tools/generator/state/secret-classifier.spec.ts +++ b/libs/common/src/tools/generator/state/secret-classifier.spec.ts @@ -77,6 +77,15 @@ describe("SecretClassifier", () => { expect(classified.disclosed).toEqual({ foo: true }); }); + it("jsonifies its outputs", () => { + const classifier = SecretClassifier.allSecret<{ foo: Date; bar: Date }>().disclose("foo"); + + const classified = classifier.classify({ foo: new Date(100), bar: new Date(100) }); + + expect(classified.disclosed).toEqual({ foo: "1970-01-01T00:00:00.100Z" }); + expect(classified.secret).toEqual({ bar: "1970-01-01T00:00:00.100Z" }); + }); + it("deletes disclosed properties from the secret member", () => { const classifier = SecretClassifier.allSecret<{ foo: boolean; bar: boolean }>().disclose( "foo", @@ -106,15 +115,6 @@ describe("SecretClassifier", () => { expect(classified.disclosed).toEqual({}); }); - - it("returns its input as the secret member", () => { - const classifier = SecretClassifier.allSecret<{ foo: boolean }>(); - const input = { foo: true }; - - const classified = classifier.classify(input); - - expect(classified.secret).toEqual(input); - }); }); describe("declassify", () => { diff --git a/libs/common/src/tools/generator/state/secret-classifier.ts b/libs/common/src/tools/generator/state/secret-classifier.ts index 232a31c686a2..a26b01ac5ddc 100644 --- a/libs/common/src/tools/generator/state/secret-classifier.ts +++ b/libs/common/src/tools/generator/state/secret-classifier.ts @@ -77,17 +77,19 @@ export class SecretClassifier { } /** Partitions `secret` into its disclosed properties and secret properties. - * @param secret The object to partition + * @param value The object to partition * @returns an object that classifies secrets. * The `disclosed` member is new and contains disclosed properties. - * The `secret` member aliases the secret parameter, with all - * disclosed and excluded properties deleted. + * The `secret` member is a copy of the secret parameter, including its + * prototype, with all disclosed and excluded properties deleted. */ - classify(secret: Plaintext): { disclosed: Disclosed; secret: Secret } { - const copy = { ...secret }; + classify(value: Plaintext): { disclosed: Jsonify<Disclosed>; secret: Jsonify<Secret> } { + // need to JSONify during classification because the prototype is almost guaranteed + // to be invalid when this method deletes arbitrary properties. + const secret = JSON.parse(JSON.stringify(value)) as Record<keyof Plaintext, unknown>; for (const excludedProp of this.excluded) { - delete copy[excludedProp]; + delete secret[excludedProp]; } const disclosed: Record<PropertyKey, unknown> = {}; @@ -95,13 +97,13 @@ export class SecretClassifier<Plaintext extends object, Disclosed, Secret> { // disclosedProp is known to be a subset of the keys of `Plaintext`, so these // type assertions are accurate. // FIXME: prove it to the compiler - disclosed[disclosedProp] = copy[disclosedProp as unknown as keyof Plaintext]; - delete copy[disclosedProp as unknown as keyof Plaintext]; + disclosed[disclosedProp] = secret[disclosedProp as keyof Plaintext]; + delete secret[disclosedProp as keyof Plaintext]; } return { - disclosed: disclosed as Disclosed, - secret: copy as unknown as Secret, + disclosed: disclosed as Jsonify<Disclosed>, + secret: secret as Jsonify<Secret>, }; } diff --git a/libs/common/src/tools/generator/state/secret-key-definition.spec.ts b/libs/common/src/tools/generator/state/secret-key-definition.spec.ts index 20bc1f5ee17b..7352631ff6ca 100644 --- a/libs/common/src/tools/generator/state/secret-key-definition.spec.ts +++ b/libs/common/src/tools/generator/state/secret-key-definition.spec.ts @@ -7,6 +7,28 @@ describe("SecretKeyDefinition", () => { const classifier = SecretClassifier.allSecret<{ foo: boolean }>(); const options = { deserializer: (v: any) => v }; + it("toEncryptedStateKey returns a key", () => { + const expectedOptions = { + deserializer: (v: any) => v, + cleanupDelayMs: 100, + }; + const definition = SecretKeyDefinition.value( + GENERATOR_DISK, + "key", + classifier, + expectedOptions, + ); + const expectedDeserializerResult = {} as any; + + const result = definition.toEncryptedStateKey(); + const deserializerResult = result.deserializer(expectedDeserializerResult); + + expect(result.stateDefinition).toEqual(GENERATOR_DISK); + expect(result.key).toBe("key"); + expect(result.cleanupDelayMs).toBe(expectedOptions.cleanupDelayMs); + expect(deserializerResult).toBe(expectedDeserializerResult); + }); + describe("value", () => { it("returns an initialized SecretKeyDefinition", () => { const definition = SecretKeyDefinition.value(GENERATOR_DISK, "key", classifier, options); diff --git a/libs/common/src/tools/generator/state/secret-key-definition.ts b/libs/common/src/tools/generator/state/secret-key-definition.ts index eb139efbe7a6..0de59be6244a 100644 --- a/libs/common/src/tools/generator/state/secret-key-definition.ts +++ b/libs/common/src/tools/generator/state/secret-key-definition.ts @@ -1,6 +1,7 @@ -import { KeyDefinitionOptions } from "../../../platform/state"; +import { KeyDefinition, KeyDefinitionOptions } from "../../../platform/state"; // eslint-disable-next-line -- `StateDefinition` used as an argument import { StateDefinition } from "../../../platform/state/state-definition"; +import { ClassifiedFormat } from "./classified-format"; import { SecretClassifier } from "./secret-classifier"; /** Encryption and storage settings for data stored by a `SecretState`. @@ -18,6 +19,20 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec readonly reconstruct: ([inners, ids]: (readonly [Id, any])[]) => Outer, ) {} + /** Converts the secret key to the `KeyDefinition` used for secret storage. */ + toEncryptedStateKey() { + const secretKey = new KeyDefinition<ClassifiedFormat<Id, Disclosed>[]>( + this.stateDefinition, + this.key, + { + cleanupDelayMs: this.options.cleanupDelayMs, + deserializer: (jsonValue) => jsonValue as ClassifiedFormat<Id, Disclosed>[], + }, + ); + + return secretKey; + } + /** * Define a secret state for a single value * @param stateDefinition The domain of the secret's durable state. diff --git a/libs/common/src/tools/generator/state/secret-state.spec.ts b/libs/common/src/tools/generator/state/secret-state.spec.ts index 364116fed3b2..1f5e14dde93d 100644 --- a/libs/common/src/tools/generator/state/secret-state.spec.ts +++ b/libs/common/src/tools/generator/state/secret-state.spec.ts @@ -36,26 +36,26 @@ const FOOBAR_RECORD = SecretKeyDefinition.record(GENERATOR_DISK, "fooBar", class const SomeUser = "some user" as UserId; -function mockEncryptor(fooBar: FooBar[] = []): UserEncryptor<FooBar> { +function mockEncryptor<T>(fooBar: T[] = []): UserEncryptor { // stores "encrypted values" so that they can be "decrypted" later // while allowing the operations to be interleaved. const encrypted = new Map<string, Jsonify<FooBar>>( - fooBar.map((fb) => [toKey(fb).encryptedString, toValue(fb)] as const), + fooBar.map((fb) => [toKey(fb as any).encryptedString, toValue(fb)] as const), ); - const result = mock<UserEncryptor<FooBar>>({ - encrypt(value: FooBar, user: UserId) { - const encString = toKey(value); + const result = mock<UserEncryptor>({ + encrypt<T>(value: Jsonify<T>, user: UserId) { + const encString = toKey(value as any); encrypted.set(encString.encryptedString, toValue(value)); return Promise.resolve(encString); }, decrypt(secret: EncString, userId: UserId) { - const decString = encrypted.get(toValue(secret.encryptedString)); - return Promise.resolve(decString); + const decValue = encrypted.get(secret.encryptedString); + return Promise.resolve(decValue as any); }, }); - function toKey(value: FooBar) { + function toKey(value: Jsonify<T>) { // `stringify` is only relevant for its uniqueness as a key // to `encrypted`. return makeEncString(JSON.stringify(value)); @@ -68,7 +68,7 @@ function mockEncryptor(fooBar: FooBar[] = []): UserEncryptor<FooBar> { // typescript pops a false positive about missing `encrypt` and `decrypt` // functions, so assert the type manually. - return result as unknown as UserEncryptor<FooBar>; + return result as unknown as UserEncryptor; } async function fakeStateProvider() { @@ -77,7 +77,7 @@ async function fakeStateProvider() { return stateProvider; } -describe("UserEncryptor", () => { +describe("SecretState", () => { describe("from", () => { it("returns a state store", async () => { const provider = await fakeStateProvider(); diff --git a/libs/common/src/tools/generator/state/secret-state.ts b/libs/common/src/tools/generator/state/secret-state.ts index a879b9f7889e..dc4ee119a60a 100644 --- a/libs/common/src/tools/generator/state/secret-state.ts +++ b/libs/common/src/tools/generator/state/secret-state.ts @@ -1,11 +1,7 @@ -import { Observable, concatMap, of, zip, map } from "rxjs"; -import { Jsonify } from "type-fest"; +import { Observable, map, concatMap, share, ReplaySubject, timer } from "rxjs"; import { EncString } from "../../../platform/models/domain/enc-string"; import { - DeriveDefinition, - DerivedState, - KeyDefinition, SingleUserState, StateProvider, StateUpdateOptions, @@ -13,28 +9,11 @@ import { } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { ClassifiedFormat } from "./classified-format"; import { SecretKeyDefinition } from "./secret-key-definition"; import { UserEncryptor } from "./user-encryptor.abstraction"; -/** Describes the structure of data stored by the SecretState's - * encrypted state. Notably, this interface ensures that `Disclosed` - * round trips through JSON serialization. It also preserves the - * Id. - * @remarks Tuple representation chosen because it matches - * `Object.entries` format. - */ -type ClassifiedFormat<Id, Disclosed> = { - /** Identifies records. `null` when storing a `value` */ - readonly id: Id | null; - /** Serialized {@link EncString} of the secret state's - * secret-level classified data. - */ - readonly secret: string; - /** serialized representation of the secret state's - * disclosed-level classified data. - */ - readonly disclosed: Jsonify<Disclosed>; -}; +const ONE_MINUTE = 1000 * 60; /** Stores account-specific secrets protected by a UserKeyEncryptor. * @@ -51,17 +30,34 @@ export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret> // wiring the derived and secret states together. private constructor( private readonly key: SecretKeyDefinition<Outer, Id, Plaintext, Disclosed, Secret>, - private readonly encryptor: UserEncryptor<Secret>, - private readonly encrypted: SingleUserState<ClassifiedFormat<Id, Disclosed>[]>, - private readonly plaintext: DerivedState<Outer>, + private readonly encryptor: UserEncryptor, + userId: UserId, + provider: StateProvider, ) { - this.state$ = plaintext.state$; - this.combinedState$ = plaintext.state$.pipe(map((state) => [this.encrypted.userId, state])); + // construct the backing store + this.encryptedState = provider.getUser(userId, key.toEncryptedStateKey()); + + // cache plaintext + this.combinedState$ = this.encryptedState.combinedState$.pipe( + concatMap( + async ([userId, state]) => [userId, await this.declassifyAll(state)] as [UserId, Outer], + ), + share({ + connector: () => { + return new ReplaySubject<[UserId, Outer]>(1); + }, + resetOnRefCountZero: () => timer(key.options.cleanupDelayMs ?? ONE_MINUTE), + }), + ); + + this.state$ = this.combinedState$.pipe(map(([, state]) => state)); } + private readonly encryptedState: SingleUserState<ClassifiedFormat<Id, Disclosed>[]>; + /** {@link SingleUserState.userId} */ get userId() { - return this.encrypted.userId; + return this.encryptedState.userId; } /** Observes changes to the decrypted secret state. The observer @@ -89,67 +85,71 @@ export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret> userId: UserId, key: SecretKeyDefinition<Outer, Id, TFrom, Disclosed, Secret>, provider: StateProvider, - encryptor: UserEncryptor<Secret>, + encryptor: UserEncryptor, ) { - // construct encrypted backing store while avoiding collisions between the derived key and the - // backing storage key. - const secretKey = new KeyDefinition<ClassifiedFormat<Id, Disclosed>[]>( - key.stateDefinition, - key.key, - { - cleanupDelayMs: key.options.cleanupDelayMs, - // FIXME: When the fakes run deserializers and serialization can be guaranteed through - // state providers, decode `jsonValue.secret` instead of it running in `derive`. - deserializer: (jsonValue) => jsonValue as ClassifiedFormat<Id, Disclosed>[], - }, - ); - const encryptedState = provider.getUser(userId, secretKey); - - // construct plaintext store - const plaintextDefinition = DeriveDefinition.from<ClassifiedFormat<Id, Disclosed>[], Outer>( - secretKey, - { - derive: async (from) => { - // fail fast if there's no value - if (from === null || from === undefined) { - return null; - } - - // decrypt each item - const decryptTasks = from.map(async ({ id, secret, disclosed }) => { - const encrypted = EncString.fromJSON(secret); - const decrypted = await encryptor.decrypt(encrypted, encryptedState.userId); - - const declassified = key.classifier.declassify(disclosed, decrypted); - const result = key.options.deserializer(declassified); - - return [id, result] as const; - }); - - // reconstruct expected type - const results = await Promise.all(decryptTasks); - const result = key.reconstruct(results); - - return result; - }, - // wire in the caller's deserializer for memory serialization - deserializer: (d) => { - const items = key.deconstruct(d); - const results = items.map(([k, v]) => [k, key.options.deserializer(v)] as const); - const result = key.reconstruct(results); - return result; - }, - // cache the decrypted data in memory - cleanupDelayMs: key.options.cleanupDelayMs, - }, - ); - const plaintextState = provider.getDerived(encryptedState.state$, plaintextDefinition, null); - - // wrap the encrypted and plaintext states in a `SecretState` facade - const secretState = new SecretState(key, encryptor, encryptedState, plaintextState); + const secretState = new SecretState(key, encryptor, userId, provider); return secretState; } + private async declassifyItem({ id, secret, disclosed }: ClassifiedFormat<Id, Disclosed>) { + const encrypted = EncString.fromJSON(secret); + const decrypted = await this.encryptor.decrypt(encrypted, this.encryptedState.userId); + + const declassified = this.key.classifier.declassify(disclosed, decrypted); + const result = [id, this.key.options.deserializer(declassified)] as const; + + return result; + } + + private async declassifyAll(data: ClassifiedFormat<Id, Disclosed>[]) { + // fail fast if there's no value + if (data === null || data === undefined) { + return null; + } + + // decrypt each item + const decryptTasks = data.map(async (item) => this.declassifyItem(item)); + + // reconstruct expected type + const results = await Promise.all(decryptTasks); + const result = this.key.reconstruct(results); + + return result; + } + + private async classifyItem([id, item]: [Id, Plaintext]) { + const classified = this.key.classifier.classify(item); + const encrypted = await this.encryptor.encrypt(classified.secret, this.encryptedState.userId); + + // the deserializer in the plaintextState's `derive` configuration always runs, but + // `encryptedState` is not guaranteed to serialize the data, so it's necessary to + // round-trip `encrypted` proactively. + const serialized = { + id, + secret: JSON.parse(JSON.stringify(encrypted)), + disclosed: classified.disclosed, + } as ClassifiedFormat<Id, Disclosed>; + + return serialized; + } + + private async classifyAll(data: Outer) { + // fail fast if there's no value + if (data === null || data === undefined) { + return null; + } + + // convert the object to a list format so that all encrypt and decrypt + // operations are self-similar + const desconstructed = this.key.deconstruct(data); + + // encrypt each value individually + const classifyTasks = desconstructed.map(async (item) => this.classifyItem(item)); + const classified = await Promise.all(classifyTasks); + + return classified; + } + /** Updates the secret stored by this state. * @param configureState a callback that returns an updated decrypted * secret state. The callback receives the state's present value as its @@ -167,71 +167,30 @@ export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret> configureState: (state: Outer, dependencies: TCombine) => Outer, options: StateUpdateOptions<Outer, TCombine> = null, ): Promise<Outer> { - // reactively grab the latest state from the caller. `zip` requires each - // observable has a value, so `combined$` provides a default if necessary. - const combined$ = options?.combineLatestWith ?? of(undefined); - const newState$ = zip(this.plaintext.state$, combined$).pipe( - concatMap(([currentState, combined]) => - this.prepareCryptoState( - currentState, - () => options?.shouldUpdate?.(currentState, combined) ?? true, - () => configureState(currentState, combined), - ), - ), - ); - - // update the backing store - let latestValue: Outer = null; - await this.encrypted.update((_, [, newStoredState]) => newStoredState, { - combineLatestWith: newState$, - shouldUpdate: (_, [shouldUpdate, , newState]) => { - // need to grab the latest value from the closure since the derived state - // could return its cached value, and this must be done in `shouldUpdate` - // because `configureState` may not run. - latestValue = newState; - return shouldUpdate; + // read the backing store + let latestClassified: ClassifiedFormat<Id, Disclosed>[]; + let latestCombined: TCombine; + await this.encryptedState.update((c) => c, { + shouldUpdate: (latest, combined) => { + latestClassified = latest; + latestCombined = combined; + return false; }, + combineLatestWith: options?.combineLatestWith, }); - return latestValue; - } - - private async prepareCryptoState( - currentState: Outer, - shouldUpdate: () => boolean, - configureState: () => Outer, - ): Promise<[boolean, ClassifiedFormat<Id, Disclosed>[], Outer]> { - // determine whether an update is necessary - if (!shouldUpdate()) { - return [false, undefined, currentState]; + // exit early if there's no update to apply + const latestDeclassified = await this.declassifyAll(latestClassified); + const shouldUpdate = options?.shouldUpdate?.(latestDeclassified, latestCombined) ?? true; + if (!shouldUpdate) { + return latestDeclassified; } - // calculate the update - const newState = configureState(); - if (newState === null || newState === undefined) { - return [true, newState as any, newState]; - } - - // convert the object to a list format so that all encrypt and decrypt - // operations are self-similar - const desconstructed = this.key.deconstruct(newState); - - // encrypt each value individually - const encryptTasks = desconstructed.map(async ([id, state]) => { - const classified = this.key.classifier.classify(state); - const encrypted = await this.encryptor.encrypt(classified.secret, this.encrypted.userId); - - // the deserializer in the plaintextState's `derive` configuration always runs, but - // `encryptedState` is not guaranteed to serialize the data, so it's necessary to - // round-trip it proactively. This will cause some duplicate work in those situations - // where the backing store does deserialize the data. - const serialized = JSON.parse( - JSON.stringify({ id, secret: encrypted, disclosed: classified.disclosed }), - ); - return serialized as ClassifiedFormat<Id, Disclosed>; - }); - const serializedState = await Promise.all(encryptTasks); + // apply the update + const updatedDeclassified = configureState(latestDeclassified, latestCombined); + const updatedClassified = await this.classifyAll(updatedDeclassified); + await this.encryptedState.update(() => updatedClassified); - return [true, serializedState, newState]; + return updatedDeclassified; } } diff --git a/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts b/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts index 2009c6f255f3..76539a0edf2d 100644 --- a/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts +++ b/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts @@ -7,7 +7,7 @@ import { UserId } from "../../../types/guid"; * user-specific information. The specific kind of information is * determined by the classification strategy. */ -export abstract class UserEncryptor<Secret> { +export abstract class UserEncryptor { /** Protects secrets in `value` with a user-specific key. * @param secret the object to protect. This object is mutated during encryption. * @param userId identifies the user-specific information used to protect @@ -17,7 +17,7 @@ export abstract class UserEncryptor<Secret> { * properties. * @throws If `value` is `null` or `undefined`, the promise rejects with an error. */ - abstract encrypt(secret: Secret, userId: UserId): Promise<EncString>; + abstract encrypt<Secret>(secret: Jsonify<Secret>, userId: UserId): Promise<EncString>; /** Combines protected secrets and disclosed data into a type that can be * rehydrated into a domain object. @@ -30,5 +30,5 @@ export abstract class UserEncryptor<Secret> { * @throws If `secret` or `disclosed` is `null` or `undefined`, the promise * rejects with an error. */ - abstract decrypt(secret: EncString, userId: UserId): Promise<Jsonify<Secret>>; + abstract decrypt<Secret>(secret: EncString, userId: UserId): Promise<Jsonify<Secret>>; } diff --git a/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts b/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts index 9289086986b4..072f7bd8f342 100644 --- a/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts +++ b/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts @@ -39,10 +39,10 @@ describe("UserKeyEncryptor", () => { it("should throw if value was not supplied", async () => { const encryptor = new UserKeyEncryptor(encryptService, keyService, dataPacker); - await expect(encryptor.encrypt(null, anyUserId)).rejects.toThrow( + await expect(encryptor.encrypt<Record<string, never>>(null, anyUserId)).rejects.toThrow( "secret cannot be null or undefined", ); - await expect(encryptor.encrypt(undefined, anyUserId)).rejects.toThrow( + await expect(encryptor.encrypt<Record<string, never>>(undefined, anyUserId)).rejects.toThrow( "secret cannot be null or undefined", ); }); @@ -50,10 +50,10 @@ describe("UserKeyEncryptor", () => { it("should throw if userId was not supplied", async () => { const encryptor = new UserKeyEncryptor(encryptService, keyService, dataPacker); - await expect(encryptor.encrypt({} as any, null)).rejects.toThrow( + await expect(encryptor.encrypt({}, null)).rejects.toThrow( "userId cannot be null or undefined", ); - await expect(encryptor.encrypt({} as any, undefined)).rejects.toThrow( + await expect(encryptor.encrypt({}, undefined)).rejects.toThrow( "userId cannot be null or undefined", ); }); diff --git a/libs/common/src/tools/generator/state/user-key-encryptor.ts b/libs/common/src/tools/generator/state/user-key-encryptor.ts index 22dbd41140b6..27724d820d04 100644 --- a/libs/common/src/tools/generator/state/user-key-encryptor.ts +++ b/libs/common/src/tools/generator/state/user-key-encryptor.ts @@ -11,7 +11,7 @@ import { UserEncryptor } from "./user-encryptor.abstraction"; /** A classification strategy that protects a type's secrets by encrypting them * with a `UserKey` */ -export class UserKeyEncryptor<Secret> extends UserEncryptor<Secret> { +export class UserKeyEncryptor extends UserEncryptor { /** Instantiates the encryptor * @param encryptService protects properties of `Secret`. * @param keyService looks up the user key when protecting data. @@ -26,7 +26,7 @@ export class UserKeyEncryptor<Secret> extends UserEncryptor<Secret> { } /** {@link UserEncryptor.encrypt} */ - async encrypt(secret: Secret, userId: UserId): Promise<EncString> { + async encrypt<Secret>(secret: Jsonify<Secret>, userId: UserId): Promise<EncString> { this.assertHasValue("secret", secret); this.assertHasValue("userId", userId); @@ -42,7 +42,7 @@ export class UserKeyEncryptor<Secret> extends UserEncryptor<Secret> { } /** {@link UserEncryptor.decrypt} */ - async decrypt(secret: EncString, userId: UserId): Promise<Jsonify<Secret>> { + async decrypt<Secret>(secret: EncString, userId: UserId): Promise<Jsonify<Secret>> { this.assertHasValue("secret", secret); this.assertHasValue("userId", userId); From 3d19e3489c0cee2ae68aac1ea7653a9580c55860 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:50:24 -0700 Subject: [PATCH 41/51] [PM-5269] Key Connector state migration (#8327) * key connector migration initial * migrator complete * fix dependencies * finalized tests * fix deps and sync main * clean up definition file * fixing tests * fixed tests * fixing CLI, Browser, Desktop builds * fixed factory options * reverting exports * implemented UserKeyDefinition clearOn * Update KeyConnector MIgration * updated migrator and tests to match profile object * removed unused service and updated clear * dep fix * dep fixes * clear usesKeyConnector on logout --- .../key-connector-service.factory.ts | 12 +- .../browser/context-menu-clicked-handler.ts | 5 +- .../browser/src/background/main.background.ts | 3 +- apps/cli/src/bw.ts | 2 +- apps/desktop/src/app/app.component.ts | 1 - apps/web/src/app/app.component.ts | 1 - .../src/services/jslib-services.module.ts | 2 +- .../abstractions/key-connector.service.ts | 1 - .../services/key-connector.service.spec.ts | 376 ++++++++++++++++++ .../auth/services/key-connector.service.ts | 58 ++- .../platform/abstractions/state.service.ts | 4 - .../src/platform/models/domain/account.ts | 2 - .../src/platform/services/state.service.ts | 34 -- .../src/platform/state/state-definitions.ts | 1 + libs/common/src/state-migrations/migrate.ts | 7 +- ...ve-key-connector-to-state-provider.spec.ts | 174 ++++++++ ...50-move-key-connector-to-state-provider.ts | 78 ++++ 17 files changed, 690 insertions(+), 71 deletions(-) create mode 100644 libs/common/src/auth/services/key-connector.service.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts diff --git a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts index 5fd1866c8304..4a0dd07b322c 100644 --- a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts @@ -27,9 +27,9 @@ import { LogServiceInitOptions, } from "../../../platform/background/service-factories/log-service.factory"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory"; @@ -40,13 +40,13 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & - StateServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & LogServiceInitOptions & OrganizationServiceInitOptions & - KeyGenerationServiceInitOptions; + KeyGenerationServiceInitOptions & + StateProviderInitOptions; export function keyConnectorServiceFactory( cache: { keyConnectorService?: AbstractKeyConnectorService } & CachedServices, @@ -58,7 +58,6 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( - await stateServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), @@ -66,6 +65,7 @@ export function keyConnectorServiceFactory( await organizationServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), opts.keyConnectorServiceOptions.logoutCallback, + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 760b833044e2..596d6b7235e2 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -30,6 +30,7 @@ import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; +import { KeyConnectorServiceInitOptions } from "../../auth/background/service-factories/key-connector-service.factory"; import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory"; @@ -78,7 +79,9 @@ export class ContextMenuClickedHandler { static async mv3Create(cachedServices: CachedServices) { const stateFactory = new StateFactory(GlobalState, Account); - const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = { + const serviceOptions: AuthServiceInitOptions & + CipherServiceInitOptions & + KeyConnectorServiceInitOptions = { apiServiceOptions: { logoutCallback: NOT_IMPLEMENTED, }, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c2c8c5be724a..5bb47ab68a14 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -514,7 +514,6 @@ export default class MainBackground { this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( - this.stateService, this.cryptoService, this.apiService, this.tokenService, @@ -522,6 +521,7 @@ export default class MainBackground { this.organizationService, this.keyGenerationService, logoutCallback, + this.stateProvider, ); this.passwordStrengthService = new PasswordStrengthService(); @@ -1125,7 +1125,6 @@ export default class MainBackground { this.policyService.clear(userId), this.passwordGenerationService.clear(userId), this.vaultTimeoutSettingsService.clear(userId), - this.keyConnectorService.clear(), this.vaultFilterService.clear(), this.biometricStateService.logout(userId), this.providerService.save(null, userId), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ce2152ffbf3f..7f23e6f2d0fa 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -427,7 +427,6 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( - this.stateService, this.cryptoService, this.apiService, this.tokenService, @@ -435,6 +434,7 @@ export class Main { this.organizationService, this.keyGenerationService, async (expired: boolean) => await this.logout(), + this.stateProvider, ); this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 196bebfcf74d..4e74135c4985 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -584,7 +584,6 @@ export class AppComponent implements OnInit, OnDestroy { await this.passwordGenerationService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); await this.policyService.clear(userBeingLoggedOut); - await this.keyConnectorService.clear(); await this.biometricStateService.logout(userBeingLoggedOut as UserId); await this.providerService.save(null, userBeingLoggedOut as UserId); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 23b45618c680..32f4ee67e207 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -276,7 +276,6 @@ export class AppComponent implements OnDestroy, OnInit { this.collectionService.clear(userId), this.policyService.clear(userId), this.passwordGenerationService.clear(), - this.keyConnectorService.clear(), this.biometricStateService.logout(userId as UserId), this.paymentMethodWarningService.clear(), ]); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b2f5ee1f8986..841edb4289a2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -765,7 +765,6 @@ const safeProviders: SafeProvider[] = [ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ - StateServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -773,6 +772,7 @@ const safeProviders: SafeProvider[] = [ OrganizationServiceAbstraction, KeyGenerationServiceAbstraction, LOGOUT_CALLBACK, + StateProvider, ], }), safeProvider({ diff --git a/libs/common/src/auth/abstractions/key-connector.service.ts b/libs/common/src/auth/abstractions/key-connector.service.ts index b7c8d5d0d0bb..36f413d70c75 100644 --- a/libs/common/src/auth/abstractions/key-connector.service.ts +++ b/libs/common/src/auth/abstractions/key-connector.service.ts @@ -15,5 +15,4 @@ export abstract class KeyConnectorService { setConvertAccountRequired: (status: boolean) => Promise<void>; getConvertAccountRequired: () => Promise<boolean>; removeConvertAccountRequired: () => Promise<void>; - clear: () => Promise<void>; } diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts new file mode 100644 index 000000000000..50fed856f973 --- /dev/null +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -0,0 +1,376 @@ +import { mock } from "jest-mock-extended"; + +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; +import { ApiService } from "../../abstractions/api.service"; +import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationData } from "../../admin-console/models/data/organization.data"; +import { Organization } from "../../admin-console/models/domain/organization"; +import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { LogService } from "../../platform/abstractions/log.service"; +import { Utils } from "../../platform/misc/utils"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { KeyGenerationService } from "../../platform/services/key-generation.service"; +import { OrganizationId, UserId } from "../../types/guid"; +import { MasterKey } from "../../types/key"; +import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; +import { KeyConnectorUserKeyResponse } from "../models/response/key-connector-user-key.response"; + +import { + USES_KEY_CONNECTOR, + CONVERT_ACCOUNT_TO_KEY_CONNECTOR, + KeyConnectorService, +} from "./key-connector.service"; +import { TokenService } from "./token.service"; + +describe("KeyConnectorService", () => { + let keyConnectorService: KeyConnectorService; + + const cryptoService = mock<CryptoService>(); + const apiService = mock<ApiService>(); + const tokenService = mock<TokenService>(); + const logService = mock<LogService>(); + const organizationService = mock<OrganizationService>(); + const keyGenerationService = mock<KeyGenerationService>(); + + let stateProvider: FakeStateProvider; + + let accountService: FakeAccountService; + + const mockUserId = Utils.newGuid() as UserId; + const mockOrgId = Utils.newGuid() as OrganizationId; + + const mockMasterKeyResponse: KeyConnectorUserKeyResponse = new KeyConnectorUserKeyResponse({ + key: "eO9nVlVl3I3sU6O+CyK0kEkpGtl/auT84Hig2WTXmZtDTqYtKpDvUPfjhgMOHf+KQzx++TVS2AOLYq856Caa7w==", + }); + + beforeEach(() => { + jest.clearAllMocks(); + + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + keyConnectorService = new KeyConnectorService( + cryptoService, + apiService, + tokenService, + logService, + organizationService, + keyGenerationService, + async () => {}, + stateProvider, + ); + }); + + it("instantiates", () => { + expect(keyConnectorService).not.toBeFalsy(); + }); + + describe("setUsesKeyConnector()", () => { + it("should update the usesKeyConnectorState with the provided value", async () => { + const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); + state.nextState(false); + + const newValue = true; + + await keyConnectorService.setUsesKeyConnector(newValue); + + expect(await keyConnectorService.getUsesKeyConnector()).toBe(newValue); + }); + }); + + describe("getManagingOrganization()", () => { + it("should return the managing organization with key connector enabled", async () => { + // Arrange + const orgs = [ + organizationData(true, true, "https://key-connector-url.com", 2, false), + organizationData(false, true, "https://key-connector-url.com", 2, false), + organizationData(true, false, "https://key-connector-url.com", 2, false), + organizationData(true, true, "https://other-url.com", 2, false), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toEqual(orgs[0]); + }); + + it("should return undefined if no managing organization with key connector enabled is found", async () => { + // Arrange + const orgs = [ + organizationData(true, false, "https://key-connector-url.com", 2, false), + organizationData(false, false, "https://key-connector-url.com", 2, false), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toBeUndefined(); + }); + + it("should return undefined if user is Owner or Admin", async () => { + // Arrange + const orgs = [ + organizationData(true, true, "https://key-connector-url.com", 0, false), + organizationData(true, true, "https://key-connector-url.com", 1, false), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toBeUndefined(); + }); + + it("should return undefined if user is a Provider", async () => { + // Arrange + const orgs = [ + organizationData(true, true, "https://key-connector-url.com", 2, true), + organizationData(false, true, "https://key-connector-url.com", 2, true), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toBeUndefined(); + }); + }); + + describe("setConvertAccountRequired()", () => { + it("should update the convertAccountToKeyConnectorState with the provided value", async () => { + const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR); + state.nextState(false); + + const newValue = true; + + await keyConnectorService.setConvertAccountRequired(newValue); + + expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue); + }); + + it("should remove the convertAccountToKeyConnectorState", async () => { + const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR); + state.nextState(false); + + const newValue: boolean = null; + + await keyConnectorService.setConvertAccountRequired(newValue); + + expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue); + }); + }); + + describe("userNeedsMigration()", () => { + it("should return true if the user needs migration", async () => { + // token + tokenService.getIsExternal.mockResolvedValue(true); + + // create organization object + const data = organizationData(true, true, "https://key-connector-url.com", 2, false); + organizationService.getAll.mockResolvedValue([data]); + + // uses KeyConnector + const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); + state.nextState(false); + + const result = await keyConnectorService.userNeedsMigration(); + + expect(result).toBe(true); + }); + + it("should return false if the user does not need migration", async () => { + tokenService.getIsExternal.mockResolvedValue(false); + const data = organizationData(false, false, "https://key-connector-url.com", 2, false); + organizationService.getAll.mockResolvedValue([data]); + + const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); + state.nextState(true); + const result = await keyConnectorService.userNeedsMigration(); + + expect(result).toBe(false); + }); + }); + + describe("setMasterKeyFromUrl", () => { + it("should set the master key from the provided URL", async () => { + // Arrange + const url = "https://key-connector-url.com"; + + apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse); + + // Hard to mock these, but we can generate the same keys + const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key); + const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; + + // Act + await keyConnectorService.setMasterKeyFromUrl(url); + + // Assert + expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); + expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + }); + + it("should handle errors thrown during the process", async () => { + // Arrange + const url = "https://key-connector-url.com"; + + const error = new Error("Failed to get master key"); + apiService.getMasterKeyFromKeyConnector.mockRejectedValue(error); + jest.spyOn(logService, "error"); + + try { + // Act + await keyConnectorService.setMasterKeyFromUrl(url); + } catch { + // Assert + expect(logService.error).toHaveBeenCalledWith(error); + expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); + } + }); + }); + + describe("migrateUser()", () => { + it("should migrate the user to the key connector", async () => { + // Arrange + const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); + const masterKey = getMockMasterKey(); + const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + + jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); + jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); + jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); + + // Act + await keyConnectorService.migrateUser(); + + // Assert + expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); + expect(cryptoService.getMasterKey).toHaveBeenCalled(); + expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( + organization.keyConnectorUrl, + keyConnectorRequest, + ); + expect(apiService.postConvertToKeyConnector).toHaveBeenCalled(); + }); + + it("should handle errors thrown during migration", async () => { + // Arrange + const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); + const masterKey = getMockMasterKey(); + const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const error = new Error("Failed to post user key to key connector"); + organizationService.getAll.mockResolvedValue([organization]); + + jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); + jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); + jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error); + jest.spyOn(logService, "error"); + + try { + // Act + await keyConnectorService.migrateUser(); + } catch { + // Assert + expect(logService.error).toHaveBeenCalledWith(error); + expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); + expect(cryptoService.getMasterKey).toHaveBeenCalled(); + expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( + organization.keyConnectorUrl, + keyConnectorRequest, + ); + } + }); + }); + + function organizationData( + usesKeyConnector: boolean, + keyConnectorEnabled: boolean, + keyConnectorUrl: string, + userType: number, + isProviderUser: boolean, + ): Organization { + return new Organization( + new OrganizationData( + new ProfileOrganizationResponse({ + id: mockOrgId, + name: "TEST_KEY_CONNECTOR_ORG", + usePolicies: true, + useSso: true, + useKeyConnector: usesKeyConnector, + useScim: true, + useGroups: true, + useDirectory: true, + useEvents: true, + useTotp: true, + use2fa: true, + useApi: true, + useResetPassword: true, + useSecretsManager: true, + usePasswordManager: true, + usersGetPremium: true, + useCustomPermissions: true, + useActivateAutofillPolicy: true, + selfHost: true, + seats: 5, + maxCollections: null, + maxStorageGb: 1, + key: "super-secret-key", + status: 2, + type: userType, + enabled: true, + ssoBound: true, + identifier: "TEST_KEY_CONNECTOR_ORG", + permissions: { + accessEventLogs: false, + accessImportExport: false, + accessReports: false, + createNewCollections: false, + editAnyCollection: false, + deleteAnyCollection: false, + editAssignedCollections: false, + deleteAssignedCollections: false, + manageGroups: false, + managePolicies: false, + manageSso: false, + manageUsers: false, + manageResetPassword: false, + manageScim: false, + }, + resetPasswordEnrolled: true, + userId: mockUserId, + hasPublicAndPrivateKeys: true, + providerId: null, + providerName: null, + providerType: null, + familySponsorshipFriendlyName: null, + familySponsorshipAvailable: true, + planProductType: 3, + KeyConnectorEnabled: keyConnectorEnabled, + KeyConnectorUrl: keyConnectorUrl, + familySponsorshipLastSyncDate: null, + familySponsorshipValidUntil: null, + familySponsorshipToDelete: null, + accessSecretsManager: false, + limitCollectionCreationDeletion: true, + allowAdminAccessToAllCollectionItems: true, + flexibleCollections: false, + object: "profileOrganization", + }), + { isMember: true, isProviderUser: isProviderUser }, + ), + ); + } + + function getMockMasterKey(): MasterKey { + const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key); + const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; + return masterKey; + } +}); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index cded13a74bff..d1502ce06c3f 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "../../abstractions/api.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; @@ -5,9 +7,14 @@ import { KeysRequest } from "../../models/request/keys.request"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; -import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { + ActiveUserState, + KEY_CONNECTOR_DISK, + StateProvider, + UserKeyDefinition, +} from "../../platform/state"; import { MasterKey } from "../../types/key"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { TokenService } from "../abstractions/token.service"; @@ -16,9 +23,28 @@ import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request"; import { IdentityTokenResponse } from "../models/response/identity-token.response"; +export const USES_KEY_CONNECTOR = new UserKeyDefinition<boolean>( + KEY_CONNECTOR_DISK, + "usesKeyConnector", + { + deserializer: (usesKeyConnector) => usesKeyConnector, + clearOn: ["logout"], + }, +); + +export const CONVERT_ACCOUNT_TO_KEY_CONNECTOR = new UserKeyDefinition<boolean>( + KEY_CONNECTOR_DISK, + "convertAccountToKeyConnector", + { + deserializer: (convertAccountToKeyConnector) => convertAccountToKeyConnector, + clearOn: ["logout"], + }, +); + export class KeyConnectorService implements KeyConnectorServiceAbstraction { + private usesKeyConnectorState: ActiveUserState<boolean>; + private convertAccountToKeyConnectorState: ActiveUserState<boolean>; constructor( - private stateService: StateService, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -26,14 +52,20 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private organizationService: OrganizationService, private keyGenerationService: KeyGenerationService, private logoutCallback: (expired: boolean, userId?: string) => Promise<void>, - ) {} + private stateProvider: StateProvider, + ) { + this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR); + this.convertAccountToKeyConnectorState = this.stateProvider.getActive( + CONVERT_ACCOUNT_TO_KEY_CONNECTOR, + ); + } - setUsesKeyConnector(usesKeyConnector: boolean) { - return this.stateService.setUsesKeyConnector(usesKeyConnector); + async setUsesKeyConnector(usesKeyConnector: boolean) { + await this.usesKeyConnectorState.update(() => usesKeyConnector); } - async getUsesKeyConnector(): Promise<boolean> { - return await this.stateService.getUsesKeyConnector(); + getUsesKeyConnector(): Promise<boolean> { + return firstValueFrom(this.usesKeyConnectorState.state$); } async userNeedsMigration() { @@ -132,19 +164,15 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async setConvertAccountRequired(status: boolean) { - await this.stateService.setConvertAccountToKeyConnector(status); + await this.convertAccountToKeyConnectorState.update(() => status); } - async getConvertAccountRequired(): Promise<boolean> { - return await this.stateService.getConvertAccountToKeyConnector(); + getConvertAccountRequired(): Promise<boolean> { + return firstValueFrom(this.convertAccountToKeyConnectorState.state$); } async removeConvertAccountRequired() { - await this.stateService.setConvertAccountToKeyConnector(null); - } - - async clear() { - await this.removeConvertAccountRequired(); + await this.setConvertAccountRequired(null); } private handleKeyConnectorError(e: any) { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index b4847279c33d..0ca0615380e1 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -52,8 +52,6 @@ export abstract class StateService<T extends Account = Account> { setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>; - getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>; - setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>; /** * Gets the user's master key */ @@ -269,8 +267,6 @@ export abstract class StateService<T extends Account = Account> { getSecurityStamp: (options?: StorageOptions) => Promise<string>; setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; getUserId: (options?: StorageOptions) => Promise<string>; - getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>; - setUsesKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>; getVaultTimeout: (options?: StorageOptions) => Promise<number>; setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index d01e9d5b8df3..01660006c0dd 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -158,7 +158,6 @@ export class AccountKeys { } export class AccountProfile { - convertAccountToKeyConnector?: boolean; name?: string; email?: string; emailVerified?: boolean; @@ -166,7 +165,6 @@ export class AccountProfile { forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; - usesKeyConnector?: boolean; keyHash?: string; kdfIterations?: number; kdfMemory?: number; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index bbcc00e5629e..8c98cc346f03 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -293,23 +293,6 @@ export class StateService< ); } - async getConvertAccountToKeyConnector(options?: StorageOptions): Promise<boolean> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.convertAccountToKeyConnector; - } - - async setConvertAccountToKeyConnector(value: boolean, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.convertAccountToKeyConnector = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - /** * @deprecated Do not save the Master Key. Use the User Symmetric Key instead */ @@ -1298,23 +1281,6 @@ export class StateService< )?.profile?.userId; } - async getUsesKeyConnector(options?: StorageOptions): Promise<boolean> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.usesKeyConnector; - } - - async setUsesKeyConnector(value: boolean, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.usesKeyConnector = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getVaultTimeout(options?: StorageOptions): Promise<number> { const accountVaultTimeout = ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index b44c449c217a..35714ee7c4a1 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -35,6 +35,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); // Auth +export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); export const SSO_DISK = new StateDefinition("ssoLogin", "disk"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index b932a7186eef..60bd31d0498d 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -46,6 +46,7 @@ import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settin import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-state-provider"; import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; +import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; @@ -53,7 +54,8 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 49; +export const CURRENT_VERSION = 50; + export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -104,7 +106,8 @@ export function createMigrationBuilder() { .with(DeleteBiometricPromptCancelledData, 45, 46) .with(MoveDesktopSettingsMigrator, 46, 47) .with(MoveDdgToStateProviderMigrator, 47, 48) - .with(AccountServerConfigMigrator, 48, CURRENT_VERSION); + .with(AccountServerConfigMigrator, 48, 49) + .with(KeyConnectorMigrator, 49, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts new file mode 100644 index 000000000000..2b960808215f --- /dev/null +++ b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts @@ -0,0 +1,174 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { KeyConnectorMigrator } from "./50-move-key-connector-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + FirstAccount: { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: false, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: true, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_keyConnector_usesKeyConnector: true, + user_FirstAccount_keyConnector_convertAccountToKeyConnector: false, + user_SecondAccount_keyConnector_usesKeyConnector: true, + user_SecondAccount_keyConnector_convertAccountToKeyConnector: true, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + FirstAccount: { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const usesKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "usesKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "convertAccountToKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +describe("KeyConnectorMigrator", () => { + let helper: MockProxy<MigrationHelper>; + let sut: KeyConnectorMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 50); + sut = new KeyConnectorMigrator(49, 50); + }); + + it("should remove usesKeyConnector and convertAccountToKeyConnector from Profile", async () => { + await sut.migrate(helper); + + // Set is called 2 times even though there are 3 accounts. Since the target properties don't exist in ThirdAccount, they are not set. + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + usesKeyConnectorKeyDefinition, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + convertAccountToKeyConnectorKeyDefinition, + false, + ); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + usesKeyConnectorKeyDefinition, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + convertAccountToKeyConnectorKeyDefinition, + true, + ); + expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount"); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 50); + sut = new KeyConnectorMigrator(49, 50); + }); + + it("should null out new usesKeyConnector global value", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(4); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + usesKeyConnectorKeyDefinition, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + convertAccountToKeyConnectorKeyDefinition, + null, + ); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: false, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + usesKeyConnectorKeyDefinition, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + convertAccountToKeyConnectorKeyDefinition, + null, + ); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: true, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount"); + expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount"); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts new file mode 100644 index 000000000000..0deb7d5e2c07 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts @@ -0,0 +1,78 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + profile?: { + usesKeyConnector?: boolean; + convertAccountToKeyConnector?: boolean; + }; +}; + +const usesKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "usesKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "convertAccountToKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +export class KeyConnectorMigrator extends Migrator<49, 50> { + async migrate(helper: MigrationHelper): Promise<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + const usesKeyConnector = account?.profile?.usesKeyConnector; + const convertAccountToKeyConnector = account?.profile?.convertAccountToKeyConnector; + if (usesKeyConnector == null && convertAccountToKeyConnector == null) { + return; + } + if (usesKeyConnector != null) { + await helper.setToUser(userId, usesKeyConnectorKeyDefinition, usesKeyConnector); + delete account.profile.usesKeyConnector; + } + if (convertAccountToKeyConnector != null) { + await helper.setToUser( + userId, + convertAccountToKeyConnectorKeyDefinition, + convertAccountToKeyConnector, + ); + delete account.profile.convertAccountToKeyConnector; + } + await helper.set(userId, account); + } + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + const usesKeyConnector: boolean = await helper.getFromUser( + userId, + usesKeyConnectorKeyDefinition, + ); + const convertAccountToKeyConnector: boolean = await helper.getFromUser( + userId, + convertAccountToKeyConnectorKeyDefinition, + ); + if (usesKeyConnector == null && convertAccountToKeyConnector == null) { + return; + } + if (usesKeyConnector != null) { + account.profile.usesKeyConnector = usesKeyConnector; + await helper.setToUser(userId, usesKeyConnectorKeyDefinition, null); + } + if (convertAccountToKeyConnector != null) { + account.profile.convertAccountToKeyConnector = convertAccountToKeyConnector; + await helper.setToUser(userId, convertAccountToKeyConnectorKeyDefinition, null); + } + await helper.set(userId, account); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} From b70897a441211c336084fd9a650fec78d353e345 Mon Sep 17 00:00:00 2001 From: Addison Beck <github@addisonbeck.com> Date: Thu, 28 Mar 2024 14:12:52 -0500 Subject: [PATCH 42/51] Await `this.getScimEndpointUrl()` (#8532) --- .../app/admin-console/organizations/manage/scim.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts index a22de64c391a..8e8db457e50a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts @@ -107,7 +107,7 @@ export class ScimComponent implements OnInit { try { const response = await this.rotatePromise; this.formData.setValue({ - endpointUrl: this.getScimEndpointUrl(), + endpointUrl: await this.getScimEndpointUrl(), clientSecret: response.apiKey, }); this.platformUtilsService.showToast("success", null, this.i18nService.t("scimApiKeyRotated")); From 7021e944752e8d8bef6945370b64b3fc2f6a1c28 Mon Sep 17 00:00:00 2001 From: watsondm <129207532+watsondm@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:44:40 -0400 Subject: [PATCH 43/51] CLOUDOPS-1369 Remove R2 bucket secrets and step from artifacts (#8534) --- .github/workflows/release-desktop-beta.yml | 20 +------------------- .github/workflows/release-desktop.yml | 21 +-------------------- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index b9e2d7a8c851..46f4ffad57da 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -955,11 +955,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 @@ -985,20 +981,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Update deployment status to Success if: ${{ success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index cf857d717724..dc6957d00d62 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -115,11 +115,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -169,21 +165,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: From ebe5a46b57bcfca686fe1f15bc7fb59ed17cbc6e Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:56:02 -0400 Subject: [PATCH 44/51] PM-5263 - Clear all tokens on logout (#8536) --- libs/common/src/auth/services/token.state.ts | 4 ++++ libs/common/src/platform/services/state.service.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 55471e1627a5..368f3c4ca29b 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -1,5 +1,9 @@ import { KeyDefinition, TOKEN_DISK, TOKEN_DISK_LOCAL, TOKEN_MEMORY } from "../../platform/state"; +// Note: all tokens / API key information must be cleared on logout. +// because we are using secure storage, we must manually call to clean up our tokens. +// See stateService.deAuthenticateAccount for where we call clearTokens(...) + export const ACCESS_TOKEN_DISK = new KeyDefinition<string>(TOKEN_DISK, "accessToken", { deserializer: (accessToken) => accessToken, }); diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 8c98cc346f03..d4297ecf94ef 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1729,7 +1729,9 @@ export class StateService< } protected async deAuthenticateAccount(userId: string): Promise<void> { - await this.tokenService.clearAccessToken(userId as UserId); + // We must have a manual call to clear tokens as we can't leverage state provider to clean + // up our data as we have secure storage in the mix. + await this.tokenService.clearTokens(userId as UserId); await this.setLastActive(null, { userId: userId }); await this.updateState(async (state) => { state.authenticatedAccounts = state.authenticatedAccounts.filter((id) => id !== userId); From 813dd97fed81ef6df40db6dd5172cc4155780782 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:08:41 +0000 Subject: [PATCH 45/51] Removing clientSideOnlyVerification on UserVerificationDialogComponent on web export.component (#8545) --- apps/web/src/app/tools/vault-export/export.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/app/tools/vault-export/export.component.ts b/apps/web/src/app/tools/vault-export/export.component.ts index 3f57f9aa71c8..4fdd3ff9e08c 100644 --- a/apps/web/src/app/tools/vault-export/export.component.ts +++ b/apps/web/src/app/tools/vault-export/export.component.ts @@ -95,7 +95,6 @@ export class ExportComponent extends BaseExportComponent { } const result = await UserVerificationDialogComponent.open(this.dialogService, { - clientSideOnlyVerification: true, title: "confirmVaultExport", bodyText: confirmDescription, confirmButtonOptions: { From 07c172d3a3431bd3e45c9967796f7548780e8738 Mon Sep 17 00:00:00 2001 From: watsondm <129207532+watsondm@users.noreply.github.com> Date: Fri, 29 Mar 2024 09:43:06 -0400 Subject: [PATCH 46/51] Revert "CLOUDOPS-1369 Remove R2 bucket secrets and step from artifacts (#8534)" (#8546) This reverts commit 7021e944752e8d8bef6945370b64b3fc2f6a1c28. --- .github/workflows/release-desktop-beta.yml | 20 +++++++++++++++++++- .github/workflows/release-desktop.yml | 21 ++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 46f4ffad57da..b9e2d7a8c851 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -955,7 +955,11 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name" + aws-electron-bucket-name, + r2-electron-access-id, + r2-electron-access-key, + r2-electron-bucket-name, + cf-prod-account" - name: Download all artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 @@ -981,6 +985,20 @@ jobs: --recursive \ --quiet + - name: Publish artifacts to R2 + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} + AWS_DEFAULT_REGION: 'us-east-1' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} + CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} + working-directory: apps/desktop/artifacts + run: | + aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ + --recursive \ + --quiet \ + --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com + - name: Update deployment status to Success if: ${{ success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index dc6957d00d62..cf857d717724 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -115,7 +115,11 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name" + aws-electron-bucket-name, + r2-electron-access-id, + r2-electron-access-key, + r2-electron-bucket-name, + cf-prod-account" - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -165,6 +169,21 @@ jobs: --recursive \ --quiet + - name: Publish artifacts to R2 + if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} + AWS_DEFAULT_REGION: 'us-east-1' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} + CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} + working-directory: apps/desktop/artifacts + run: | + aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ + --recursive \ + --quiet \ + --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com + - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: From 3a830789babbdc8f33ee18bb785e1d70518204fc Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Fri, 29 Mar 2024 10:06:50 -0400 Subject: [PATCH 47/51] [PM-5884] Allow deletion of passkey from edit view - clients (#8500) * add remove button for passkeys during edit * added live region to announce when a passkey is removed * removed announce passkey removed by SR * removed unused variable --- apps/browser/src/_locales/en/messages.json | 6 ++++++ .../components/vault/add-edit.component.html | 18 ++++++++++++++---- apps/desktop/src/locales/en/messages.json | 6 ++++++ .../vault/app/vault/add-edit.component.html | 17 ++++++++++++++--- .../individual-vault/add-edit.component.html | 15 ++++++++++++--- .../src/vault/components/add-edit.component.ts | 8 ++++++++ 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 0defc8aa7c6b..d802d2770018 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html index ecdeb9cda724..8ff448b6f760 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html @@ -138,10 +138,20 @@ <h2 class="box-header"> attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}" > <div class="box-content"> - <div class="box-content-row text-muted"> - <span class="row-label">{{ "typePasskey" | i18n }}</span> - {{ "dateCreated" | i18n }} - {{ cipher.login.fido2Credentials[0].creationDate | date: "short" }} + <div class="box-content-row box-content-row-multi text-muted" appBoxRow> + <button + type="button" + appStopClick + (click)="removePasskey()" + appA11yTitle="{{ 'removePasskey' | i18n }}" + > + <i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i> + </button> + <div class="row-main"> + <span class="row-label">{{ "typePasskey" | i18n }}</span> + {{ "dateCreated" | i18n }} + {{ cipher.login.fido2Credentials[0].creationDate | date: "short" }} + </div> </div> </div> </div> diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3a4835f16a16..394a5951e98b 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2696,5 +2696,11 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.html b/apps/desktop/src/vault/app/vault/add-edit.component.html index 43add5325437..ea7be9293515 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit.component.html @@ -116,14 +116,25 @@ <h2 class="box-header"> </div> <!--Passkey--> <div - class="box-content-row text-muted" + class="box-content-row box-content-row-multi text-muted" *ngIf="cipher.login.hasFido2Credentials && !cloneMode" appBoxRow tabindex="0" attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}" > - <span class="row-label">{{ "typePasskey" | i18n }}</span> - {{ fido2CredentialCreationDateValue }} + <button + type="button" + appStopClick + (click)="removePasskey()" + appA11yTitle="{{ 'removePasskey' | i18n }}" + [disabled]="!cipher.edit && editMode" + > + <i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i> + </button> + <div class="row-main"> + <span class="row-label">{{ "typePasskey" | i18n }}</span> + {{ fido2CredentialCreationDateValue }} + </div> </div> <div class="box-content-row" appBoxRow> diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 12b41f181d6f..85075acfdd29 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -192,11 +192,11 @@ <h1 class="modal-title" id="cipherAddEditTitle">{{ title }}</h1> </div> </div> <ng-container *ngIf="cipher.login.hasFido2Credentials"> - <div class="row"> - <div class="col-6 form-group"> + <div class="tw-flex tw-flex-row"> + <div class="tw-mb-4 tw-w-1/2"> <label for="loginFido2credential">{{ "typePasskey" | i18n }}</label> <div - class="input-group" + class="tw-flex tw-flex-row" tabindex="0" attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue @@ -212,6 +212,15 @@ <h1 class="modal-title" id="cipherAddEditTitle">{{ title }}</h1> disabled readonly /> + <button + type="button" + class="tw-items-center tw-border-none tw-bg-transparent tw-text-danger tw-ml-3" + appA11yTitle="{{ 'removePasskey' | i18n }}" + (click)="removePasskey()" + *ngIf="!cipher.isDeleted && !viewOnly" + > + <i class="bwi bwi-lg bwi-minus-circle"></i> + </button> </div> </div> </div> diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 4f5334d176bc..4c177a77f2f9 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -402,6 +402,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } } + removePasskey() { + if (this.cipher.type !== CipherType.Login || this.cipher.login.fido2Credentials == null) { + return; + } + + this.cipher.login.fido2Credentials = null; + } + onCardNumberChange(): void { this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number); } From 9d1219bda6b6e9bfe509865f9beb65e6add46c70 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Mar 2024 15:54:20 +0000 Subject: [PATCH 48/51] Autosync the updated translations (#8541) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/store/locales/gl/copy.resx | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index 191198691d4a..d812256fb730 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -118,58 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden – Xestor de contrasinais gratuíto</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Bitwarden, Inc. é a empresa matriz de 8bit Solutions LLC. -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +NOMEADO MELLOR ADMINISTRADOR DE CONTRASINAIS POR THE VERGE, Ou.S. NEWS &amp; WORLD REPORT, CNET E MÁS. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +Administre, almacene, protexa e comparta contrasinais ilimitados en dispositivos ilimitados desde calquera lugar. Bitwarden ofrece solucións de xestión de contrasinais de código aberto para todos, xa sexa en casa, no traballo ou en mentres estás de viaxe. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +Xere contrasinais seguros, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +Bitwarden Send transmite rapidamente información cifrada --- arquivos e texto sen formato, directamente a calquera persoa. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Bitwarden ofrece plans Teams e Enterprise para empresas para que poida compartir contrasinais de forma segura con colegas. -Why Choose Bitwarden: +Por que elixir Bitwarden? -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Cifrado de clase mundial +Os contrasinais están protexidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing e PBKDF2 XA-256) para que os seus datos permanezan seguros e privados. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +Xerador de contrasinais incorporado +Xere contrasinais fortes, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Traducións Globais +As traducións de Bitwarden existen en 40 idiomas e están a crecer, grazas á nosa comunidade global. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplicacións multiplataforma +Protexa e comparta datos confidenciais dentro da súa Caixa Forte de Bitwarden desde calquera navegador, dispositivo móbil ou sistema operativo de escritorio, e máis. </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos</value> </data> <data name="ScreenshotSync" xml:space="preserve"> - <value>Sync and access your vault from multiple devices</value> + <value>Sincroniza e accede á túa caixa forte desde múltiples dispositivos</value> </data> <data name="ScreenshotVault" xml:space="preserve"> - <value>Manage all your logins and passwords from a secure vault</value> + <value>Xestiona todos os teus usuarios e contrasinais desde unha caixa forte segura</value> </data> <data name="ScreenshotAutofill" xml:space="preserve"> - <value>Quickly auto-fill your login credentials into any website that you visit</value> + <value>Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites</value> </data> <data name="ScreenshotMenu" xml:space="preserve"> - <value>Your vault is also conveniently accessible from the right-click menu</value> + <value>A túa caixa forte tamén é facilmente accesible desde o menú de clic dereito</value> </data> <data name="ScreenshotPassword" xml:space="preserve"> - <value>Automatically generate strong, random, and secure passwords</value> + <value>Xera automaticamente contrasinais fortes, aleatorias e seguras</value> </data> <data name="ScreenshotEdit" xml:space="preserve"> - <value>Your information is managed securely using AES-256 bit encryption</value> + <value>A túa información é xestionada de forma segura con cifrado AES de 256 bits</value> </data> </root> From 670f33daa8af3416e0356a0ba932d4d38d75cc79 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Fri, 29 Mar 2024 10:55:23 -0500 Subject: [PATCH 49/51] [PM-5743] Implement eslint rule for usage of window object in background script (#7849) * [PM-5742] Rework Usage of Extension APIs that Cannot be Called with the Background Service Worker * [PM-5742] Implementing jest tests for the updated BrowserApi methods * [PM-5742] Implementing jest tests to validate logic within added API calls * [PM-5742] Implementing jest tests to validate logic within added API calls * [PM-5742] Fixing broken Jest tests * [PM-5742] Fixing linter error * [PM-5887] Refactor WebCryptoFunction to Remove Usage of the window Object in the Background Script * [PM-5878] Rework `window` call within OverlayBackground to function within AutofillOverlayIframe service * [PM-6122] Rework `window` call within NotificationBackground to function within content script * [PM-5881] Adjust usage of the `chrome.extension.getViews` API to ensure expected behavior in manifest v3 * [PM-5881] Reworking how we handle early returns from `reloadOpenWindows` * [PM-5881] Implementing jest test to validate changes within BrowserApi.reloadOpenWindows * [PM-5743] Implement eslint rule to impeede usage of the `window` object in the background script * [PM-5743] Working through fixing eslint rule errors, and setting up ignore statements for lines that will be refactored at a later date * [PM-5743] Fixing broken jest tests * [PM-5879] Removing `backgroundWindow` reference used for determing system theme preference in Safari * [PM-5879] Removing `backgroundWindow` reference used for determing system theme preference in Safari * [PM-5743] Updating references to NodeJS.Timeout * [PM-5743] Adding notification bar and overaly content scripts to the eslint excluded files key * [PM-5743] Adding other excluded files from the eslint rule * [PM-5743] Reworking implementation to have the .eslintrc.json file present within the browser subdirectory --- apps/browser/.eslintrc.json | 26 +++++++++++++++++ .../background/web-request.background.ts | 2 +- .../src/autofill/content/autofiller.ts | 2 +- apps/browser/src/autofill/notification/bar.ts | 6 ++-- .../autofill-overlay-iframe.service.ts | 2 +- .../pages/list/autofill-overlay-list.ts | 2 +- .../services/abstractions/autofill.service.ts | 2 +- .../autofill-overlay-content.service.ts | 4 +-- .../src/autofill/services/autofill.service.ts | 2 +- .../collect-autofill-content.service.ts | 6 ++-- .../dom-element-visibility.service.ts | 2 +- .../insert-autofill-content.service.spec.ts | 28 +++++++++---------- .../insert-autofill-content.service.ts | 16 +++++------ .../browser/src/background/idle.background.ts | 6 ++-- .../browser/src/background/main.background.ts | 2 +- .../src/background/runtime.background.ts | 2 +- apps/browser/src/platform/background.ts | 2 +- .../src/platform/browser/browser-api.ts | 6 ++-- .../offscreen-document/offscreen-document.ts | 4 +-- .../services/browser-file-download.service.ts | 4 +-- ...ssaging-private-mode-background.service.ts | 2 +- ...er-messaging-private-mode-popup.service.ts | 2 +- apps/browser/src/popup/app.component.ts | 2 +- .../src/popup/services/services.module.ts | 2 +- 24 files changed, 79 insertions(+), 55 deletions(-) create mode 100644 apps/browser/.eslintrc.json rename apps/browser/src/platform/{ => popup}/services/browser-file-download.service.ts (93%) diff --git a/apps/browser/.eslintrc.json b/apps/browser/.eslintrc.json new file mode 100644 index 000000000000..ba960511839b --- /dev/null +++ b/apps/browser/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "webextensions": true + }, + "overrides": [ + { + "files": ["src/**/*.ts"], + "excludedFiles": [ + "src/**/{content,popup,spec}/**/*.ts", + "src/**/autofill/{notification,overlay}/**/*.ts", + "src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts", + "src/**/*.spec.ts" + ], + "rules": { + "no-restricted-globals": [ + "error", + { + "name": "window", + "message": "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead." + } + ] + } + } + ] +} diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index f4422e6d7f60..8cdfa0f0276c 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -17,7 +17,7 @@ export default class WebRequestBackground { private authService: AuthService, ) { if (BrowserApi.isManifestVersion(2)) { - this.webRequest = (window as any).chrome.webRequest; + this.webRequest = chrome.webRequest; } this.isFirefox = platformUtilsService.isFirefox(); } diff --git a/apps/browser/src/autofill/content/autofiller.ts b/apps/browser/src/autofill/content/autofiller.ts index 5f43023d8bd9..0ca9d37187ad 100644 --- a/apps/browser/src/autofill/content/autofiller.ts +++ b/apps/browser/src/autofill/content/autofiller.ts @@ -10,7 +10,7 @@ function loadAutofiller() { let pageHref: string = null; let filledThisHref = false; let delayFillTimeout: number; - let doFillInterval: NodeJS.Timeout; + let doFillInterval: number | NodeJS.Timeout; const handleExtensionDisconnect = () => { clearDoFillInterval(); clearDelayFillTimeout(); diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 5d28bf839760..a730ee1ebac9 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -139,7 +139,7 @@ function initNotificationBar(message: NotificationBarWindowMessage) { }); }); - window.addEventListener("resize", adjustHeight); + globalThis.addEventListener("resize", adjustHeight); adjustHeight(); } @@ -384,7 +384,7 @@ function setupLogoLink(i18n: Record<string, string>) { function setNotificationBarTheme() { let theme = notificationBarIframeInitData.theme; if (theme === ThemeType.System) { - theme = window.matchMedia("(prefers-color-scheme: dark)").matches + theme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? ThemeType.Dark : ThemeType.Light; } @@ -393,5 +393,5 @@ function setNotificationBarTheme() { } function postMessageToParent(message: NotificationBarWindowMessage) { - window.parent.postMessage(message, windowMessageOrigin || "*"); + globalThis.parent.postMessage(message, windowMessageOrigin || "*"); } diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts index 0ec7db131c5d..b7a6f2a39ed3 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts @@ -211,7 +211,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf let borderColor: string; let verifiedTheme = theme; if (verifiedTheme === ThemeType.System) { - verifiedTheme = window.matchMedia("(prefers-color-scheme: dark)").matches + verifiedTheme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? ThemeType.Dark : ThemeType.Light; } diff --git a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts index 305a230e5cfc..8d4fa724afe3 100644 --- a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts @@ -19,7 +19,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement { private ciphers: OverlayCipherData[] = []; private ciphersList: HTMLUListElement; private cipherListScrollIsDebounced = false; - private cipherListScrollDebounceTimeout: NodeJS.Timeout; + private cipherListScrollDebounceTimeout: number | NodeJS.Timeout; private currentCipherIndex = 0; private readonly showCiphersPerPage = 6; private readonly overlayListWindowMessageHandlers: OverlayListWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 77a5f982fd99..54a91a517649 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -15,7 +15,7 @@ export interface PageDetail { export interface AutoFillOptions { cipher: CipherView; pageDetails: PageDetail[]; - doc?: typeof window.document; + doc?: typeof self.document; tab: chrome.tabs.Tab; skipUsernameOnlyFill?: boolean; onlyEmptyFields?: boolean; diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 79abdc39381d..cd373cdfd3bf 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -712,7 +712,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private async getBoundingClientRectFromIntersectionObserver( formFieldElement: ElementWithOpId<FormFieldElement>, ): Promise<DOMRectReadOnly | null> { - if (!("IntersectionObserver" in window) && !("IntersectionObserverEntry" in window)) { + if (!("IntersectionObserver" in globalThis) && !("IntersectionObserverEntry" in globalThis)) { return null; } @@ -901,7 +901,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte if ( this.focusedFieldData.focusedFieldRects?.top > 0 && - this.focusedFieldData.focusedFieldRects?.top < window.innerHeight + this.focusedFieldData.focusedFieldRects?.top < globalThis.innerHeight ) { return; } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index e353a34ea077..dae29e61e496 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -42,7 +42,7 @@ import { export default class AutofillService implements AutofillServiceInterface { private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout; - private openPasswordRepromptPopoutDebounce: NodeJS.Timeout; + private openPasswordRepromptPopoutDebounce: number | NodeJS.Timeout; private currentlyOpeningPasswordRepromptPopout = false; private autofillScriptPortsSet = new Set<chrome.runtime.Port>(); static searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 1de801a2c2c5..3144edcda937 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -39,7 +39,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private autofillFieldElements: AutofillFieldElements = new Map(); private currentLocationHref = ""; private mutationObserver: MutationObserver; - private updateAutofillElementsAfterMutationTimeout: NodeJS.Timeout; + private updateAutofillElementsAfterMutationTimeout: number | NodeJS.Timeout; private readonly updateAfterMutationTimeoutDelay = 1000; private readonly ignoredInputTypes = new Set([ "hidden", @@ -180,7 +180,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte ): AutofillPageDetails { return { title: document.title, - url: (document.defaultView || window).location.href, + url: (document.defaultView || globalThis).location.href, documentUrl: document.location.href, forms: autofillFormsData, fields: autofillFieldsData, @@ -240,7 +240,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private getFormActionAttribute(element: ElementWithOpId<HTMLFormElement>): string { - return new URL(this.getPropertyOrAttribute(element, "action"), window.location.href).href; + return new URL(this.getPropertyOrAttribute(element, "action"), globalThis.location.href).href; } /** diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.ts index acc5b1205968..127ce84d9194 100644 --- a/apps/browser/src/autofill/services/dom-element-visibility.service.ts +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.ts @@ -66,7 +66,7 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac */ private getElementStyle(element: HTMLElement, styleProperty: string): string { if (!this.cachedComputedStyle) { - this.cachedComputedStyle = (element.ownerDocument.defaultView || window).getComputedStyle( + this.cachedComputedStyle = (element.ownerDocument.defaultView || globalThis).getComputedStyle( element, ); } diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index 5ea1284d1bb7..5a123bf835f1 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -47,7 +47,7 @@ const initEventCount = Object.freeze( ); let confirmSpy: jest.SpyInstance<boolean, [message?: string]>; -let windowSpy: jest.SpyInstance<any>; +let windowLocationSpy: jest.SpyInstance<any>; let savedURLs: string[] | null = ["https://bitwarden.com"]; function setMockWindowLocation({ protocol, @@ -56,11 +56,9 @@ function setMockWindowLocation({ protocol: "http:" | "https:"; hostname: string; }) { - windowSpy.mockImplementation(() => ({ - location: { - protocol, - hostname, - }, + windowLocationSpy.mockImplementation(() => ({ + protocol, + hostname, })); } @@ -76,8 +74,8 @@ describe("InsertAutofillContentService", () => { beforeEach(() => { document.body.innerHTML = mockLoginForm; - confirmSpy = jest.spyOn(window, "confirm"); - windowSpy = jest.spyOn(window, "window", "get"); + confirmSpy = jest.spyOn(globalThis, "confirm"); + windowLocationSpy = jest.spyOn(globalThis, "location", "get"); insertAutofillContentService = new InsertAutofillContentService( domElementVisibilityService, collectAutofillContentService, @@ -101,7 +99,7 @@ describe("InsertAutofillContentService", () => { afterEach(() => { jest.resetAllMocks(); - windowSpy.mockRestore(); + windowLocationSpy.mockRestore(); confirmSpy.mockRestore(); document.body.innerHTML = ""; }); @@ -245,8 +243,8 @@ describe("InsertAutofillContentService", () => { }); it("returns true if the frameElement has a sandbox attribute", () => { - Object.defineProperty(globalThis, "window", { - value: { frameElement: { hasAttribute: jest.fn(() => true) } }, + Object.defineProperty(globalThis, "frameElement", { + value: { hasAttribute: jest.fn(() => true) }, writable: true, }); @@ -991,11 +989,11 @@ describe("InsertAutofillContentService", () => { const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; inputElement.value = "test"; jest.spyOn(inputElement, "focus"); - jest.spyOn(window, "String"); + jest.spyOn(globalThis, "String"); insertAutofillContentService["triggerFocusOnElement"](inputElement, true); - expect(window.String).toHaveBeenCalledWith(value); + expect(globalThis.String).toHaveBeenCalledWith(value); expect(inputElement.focus).toHaveBeenCalled(); expect(inputElement.value).toEqual(value); }); @@ -1005,11 +1003,11 @@ describe("InsertAutofillContentService", () => { const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; inputElement.value = "test"; jest.spyOn(inputElement, "focus"); - jest.spyOn(window, "String"); + jest.spyOn(globalThis, "String"); insertAutofillContentService["triggerFocusOnElement"](inputElement, false); - expect(window.String).not.toHaveBeenCalledWith(); + expect(globalThis.String).not.toHaveBeenCalledWith(); expect(inputElement.focus).toHaveBeenCalled(); expect(inputElement.value).toEqual(value); }); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index dd14cadfa7be..5cfa8091c40c 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -65,8 +65,8 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf private fillingWithinSandboxedIframe() { return ( String(self.origin).toLowerCase() === "null" || - window.frameElement?.hasAttribute("sandbox") || - window.location.hostname === "" + globalThis.frameElement?.hasAttribute("sandbox") || + globalThis.location.hostname === "" ); } @@ -79,8 +79,8 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf */ private userCancelledInsecureUrlAutofill(savedUrls?: string[] | null): boolean { if ( - !savedUrls?.some((url) => url.startsWith(`https://${window.location.hostname}`)) || - window.location.protocol !== "http:" || + !savedUrls?.some((url) => url.startsWith(`https://${globalThis.location.hostname}`)) || + globalThis.location.protocol !== "http:" || !this.isPasswordFieldWithinDocument() ) { return false; @@ -88,10 +88,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf const confirmationWarning = [ chrome.i18n.getMessage("insecurePageWarning"), - chrome.i18n.getMessage("insecurePageWarningFillPrompt", [window.location.hostname]), + chrome.i18n.getMessage("insecurePageWarningFillPrompt", [globalThis.location.hostname]), ].join("\n\n"); - return !confirm(confirmationWarning); + return !globalThis.confirm(confirmationWarning); } /** @@ -129,10 +129,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf const confirmationWarning = [ chrome.i18n.getMessage("autofillIframeWarning"), - chrome.i18n.getMessage("autofillIframeWarningTip", [window.location.hostname]), + chrome.i18n.getMessage("autofillIframeWarningTip", [globalThis.location.hostname]), ].join("\n\n"); - return !confirm(confirmationWarning); + return !globalThis.confirm(confirmationWarning); } /** diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 28c056cc0e6d..7b273459ad9a 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -11,7 +11,7 @@ const IdleInterval = 60 * 5; // 5 minutes export default class IdleBackground { private idle: typeof chrome.idle | typeof browser.idle | null; - private idleTimer: number = null; + private idleTimer: number | NodeJS.Timeout = null; private idleState = "active"; constructor( @@ -73,7 +73,7 @@ export default class IdleBackground { private pollIdle(handler: (newState: string) => void) { if (this.idleTimer != null) { - window.clearTimeout(this.idleTimer); + globalThis.clearTimeout(this.idleTimer); this.idleTimer = null; } // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -83,7 +83,7 @@ export default class IdleBackground { this.idleState = state; handler(state); } - this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000); + this.idleTimer = globalThis.setTimeout(() => this.pollIdle(handler), 5000); }); } } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5bb47ab68a14..957ebd998b36 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -991,7 +991,7 @@ export default class MainBackground { } async bootstrap() { - this.containerService.attachToGlobal(window); + this.containerService.attachToGlobal(self); await this.stateService.init(); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index dd55c14fb27f..a88bc051d887 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -89,7 +89,7 @@ export default class RuntimeBackground { BrowserApi.messageListener("runtime.background", backgroundMessageListener); if (this.main.popupOnlyContext) { - (window as any).bitwardenBackgroundMessageListener = backgroundMessageListener; + (self as any).bitwardenBackgroundMessageListener = backgroundMessageListener; } } diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index b71b4d96b01d..5aa2820e5f5c 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -33,7 +33,7 @@ if (BrowserApi.isManifestVersion(3)) { }, ); } else { - const bitwardenMain = ((window as any).bitwardenMain = new MainBackground()); + const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); // 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 bitwardenMain.bootstrap().then(() => { diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 362aac1af993..b2ee66f05136 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -351,11 +351,11 @@ export class BrowserApi { private static setupUnloadListeners() { // The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well // 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one - window.onpagehide = () => { + self.addEventListener("pagehide", () => { for (const [event, callback] of BrowserApi.trackedChromeEventListeners) { event.removeListener(callback); } - }; + }); } static sendMessage(subscriber: string, arg: any = {}) { @@ -423,7 +423,7 @@ export class BrowserApi { return; } - const currentHref = window.location.href; + const currentHref = self.location.href; views .filter((w) => w.location.href != null && !w.location.href.includes("background.html")) .filter((w) => !exemptCurrentHref || w.location.href !== currentHref) diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 02ae5cdb236a..627036b80bd3 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -29,14 +29,14 @@ class OffscreenDocument implements OffscreenDocumentInterface { * @param message - The extension message containing the text to copy */ private async handleOffscreenCopyToClipboard(message: OffscreenDocumentExtensionMessage) { - await BrowserClipboardService.copy(window, message.text); + await BrowserClipboardService.copy(self, message.text); } /** * Reads the user's clipboard and returns the text. */ private async handleOffscreenReadFromClipboard() { - return await BrowserClipboardService.read(window); + return await BrowserClipboardService.read(self); } /** diff --git a/apps/browser/src/platform/services/browser-file-download.service.ts b/apps/browser/src/platform/popup/services/browser-file-download.service.ts similarity index 93% rename from apps/browser/src/platform/services/browser-file-download.service.ts rename to apps/browser/src/platform/popup/services/browser-file-download.service.ts index 8cb4d498a327..e9aaa639c420 100644 --- a/apps/browser/src/platform/services/browser-file-download.service.ts +++ b/apps/browser/src/platform/popup/services/browser-file-download.service.ts @@ -5,8 +5,8 @@ import { FileDownloadRequest } from "@bitwarden/common/platform/abstractions/fil import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SafariApp } from "../../browser/safariApp"; -import { BrowserApi } from "../browser/browser-api"; +import { SafariApp } from "../../../browser/safariApp"; +import { BrowserApi } from "../../browser/browser-api"; @Injectable() export class BrowserFileDownloadService implements FileDownloadService { diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts index c2a6f8c5e1ff..0c7008473bb6 100644 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts +++ b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts @@ -3,6 +3,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService { send(subscriber: string, arg: any = {}) { const message = Object.assign({}, { command: subscriber }, arg); - (window as any).bitwardenPopupMainMessageListener(message); + (self as any).bitwardenPopupMainMessageListener(message); } } diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts index 5572ba1ba41f..5883f6119706 100644 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts +++ b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts @@ -3,6 +3,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag export default class BrowserMessagingPrivateModePopupService implements MessagingService { send(subscriber: string, arg: any = {}) { const message = Object.assign({}, { command: subscriber }, arg); - (window as any).bitwardenBackgroundMessageListener(message); + (self as any).bitwardenBackgroundMessageListener(message); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index aec8ba7c66ba..9aa438d3b3ba 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -140,7 +140,7 @@ export class AppComponent implements OnInit, OnDestroy { } }; - (window as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; + (self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener); // eslint-disable-next-line rxjs/no-async-subscribe diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 33593b56dd40..9bdd31785480 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -92,9 +92,9 @@ import MainBackground from "../../background/main.background"; import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; -import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; import BrowserMessagingService from "../../platform/services/browser-messaging.service"; From 77cfa8a5ad9ab355896b3a5a76998d5c15c8a823 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Fri, 29 Mar 2024 14:08:46 -0500 Subject: [PATCH 50/51] [PM-7128] Fix cached form fields not showing the inline menu after their visibility is changed using CSS (#8509) --- .../autofill/content/autofill-init.spec.ts | 1 + .../autofill-overlay-content.service.spec.ts | 53 ++++++------- .../autofill-overlay-content.service.ts | 2 +- .../collect-autofill-content.service.spec.ts | 73 +++++++++++++++++- .../collect-autofill-content.service.ts | 76 ++++++++++++++++--- 5 files changed, 161 insertions(+), 44 deletions(-) diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index 8912a8c0ba37..b299ddccbff8 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -24,6 +24,7 @@ describe("AutofillInit", () => { }, }); autofillInit = new AutofillInit(autofillOverlayContentService); + window.IntersectionObserver = jest.fn(() => mock<IntersectionObserver>()); }); afterEach(() => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 9f3ffea142a8..96a1b4c8512f 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -173,12 +173,10 @@ describe("AutofillOverlayContentService", () => { autofillFieldData = mock<AutofillField>(); }); - it("ignores fields that are readonly", () => { + it("ignores fields that are readonly", async () => { autofillFieldData.readonly = true; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -186,12 +184,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that contain a disabled attribute", () => { + it("ignores fields that contain a disabled attribute", async () => { autofillFieldData.disabled = true; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -199,12 +195,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that are not viewable", () => { + it("ignores fields that are not viewable", async () => { autofillFieldData.viewable = false; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -213,12 +207,10 @@ describe("AutofillOverlayContentService", () => { }); it("ignores fields that are part of the ExcludedOverlayTypes", () => { - AutoFillConstants.ExcludedOverlayTypes.forEach((excludedType) => { + AutoFillConstants.ExcludedOverlayTypes.forEach(async (excludedType) => { autofillFieldData.type = excludedType; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -227,12 +219,10 @@ describe("AutofillOverlayContentService", () => { }); }); - it("ignores fields that contain the keyword `search`", () => { + it("ignores fields that contain the keyword `search`", async () => { autofillFieldData.placeholder = "search"; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -240,12 +230,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that contain the keyword `captcha` ", () => { + it("ignores fields that contain the keyword `captcha` ", async () => { autofillFieldData.placeholder = "captcha"; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -253,12 +241,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that do not appear as a login field", () => { + it("ignores fields that do not appear as a login field", async () => { autofillFieldData.placeholder = "not-a-login-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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -267,6 +253,17 @@ describe("AutofillOverlayContentService", () => { }); }); + it("skips setup on fields that have been previously set up", async () => { + autofillOverlayContentService["formFieldElements"].add(autofillFieldElement); + + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( + autofillFieldElement, + autofillFieldData, + ); + + expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); + }); + describe("identifies the overlay visibility setting", () => { it("defaults the overlay visibility setting to `OnFieldFocus` if a value is not set", async () => { sendExtensionMessageSpy.mockResolvedValueOnce(undefined); diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index cd373cdfd3bf..4b786e6ca31e 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -86,7 +86,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte formFieldElement: ElementWithOpId<FormFieldElement>, autofillFieldData: AutofillField, ) { - if (this.isIgnoredField(autofillFieldData)) { + if (this.isIgnoredField(autofillFieldData) || this.formFieldElements.has(formFieldElement)) { return; } diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index d5c461269b06..79cb41b9a127 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -27,6 +27,7 @@ describe("CollectAutofillContentService", () => { const domElementVisibilityService = new DomElementVisibilityService(); const autofillOverlayContentService = new AutofillOverlayContentService(); let collectAutofillContentService: CollectAutofillContentService; + const mockIntersectionObserver = mock<IntersectionObserver>(); beforeEach(() => { document.body.innerHTML = mockLoginForm; @@ -34,6 +35,7 @@ describe("CollectAutofillContentService", () => { domElementVisibilityService, autofillOverlayContentService, ); + window.IntersectionObserver = jest.fn(() => mockIntersectionObserver); }); afterEach(() => { @@ -2527,10 +2529,10 @@ describe("CollectAutofillContentService", () => { }); updatedAttributes.forEach((attribute) => { - it(`will update the ${attribute} value for the field element`, async () => { + it(`will update the ${attribute} value for the field element`, () => { jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); - await collectAutofillContentService["updateAutofillFieldElementData"]( + collectAutofillContentService["updateAutofillFieldElementData"]( attribute, fieldElement, autofillField, @@ -2543,10 +2545,10 @@ describe("CollectAutofillContentService", () => { }); }); - it("will not update an attribute value if it is not present in the updateActions object", async () => { + it("will not update an attribute value if it is not present in the updateActions object", () => { jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); - await collectAutofillContentService["updateAutofillFieldElementData"]( + collectAutofillContentService["updateAutofillFieldElementData"]( "random-attribute", fieldElement, autofillField, @@ -2555,4 +2557,67 @@ describe("CollectAutofillContentService", () => { expect(collectAutofillContentService["autofillFieldElements"].set).not.toBeCalled(); }); }); + + describe("handleFormElementIntersection", () => { + let isFormFieldViewableSpy: jest.SpyInstance; + let setupAutofillOverlayListenerOnFieldSpy: jest.SpyInstance; + + beforeEach(() => { + isFormFieldViewableSpy = jest.spyOn( + collectAutofillContentService["domElementVisibilityService"], + "isFormFieldViewable", + ); + setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( + collectAutofillContentService["autofillOverlayContentService"], + "setupAutofillOverlayListenerOnField", + ); + }); + + it("skips the initial intersection event for an observed element", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>; + collectAutofillContentService["elementInitializingIntersectionObserver"].add( + formFieldElement, + ); + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).not.toHaveBeenCalled(); + expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); + }); + + it("skips setting up the overlay listeners on a field that is not viewable", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>; + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + isFormFieldViewableSpy.mockReturnValueOnce(false); + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).toHaveBeenCalledWith(formFieldElement); + expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); + }); + + it("sets up the overlay listeners on a viewable field", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>; + const autofillField = mock<AutofillField>(); + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + isFormFieldViewableSpy.mockReturnValueOnce(true); + collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField); + collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver; + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).toHaveBeenCalledWith(formFieldElement); + expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledWith( + formFieldElement, + autofillField, + ); + }); + }); }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 3144edcda937..63dee7f3b19f 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -38,6 +38,8 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private autofillFormElements: AutofillFormElements = new Map(); private autofillFieldElements: AutofillFieldElements = new Map(); private currentLocationHref = ""; + private intersectionObserver: IntersectionObserver; + private elementInitializingIntersectionObserver: Set<Element> = new Set(); private mutationObserver: MutationObserver; private updateAutofillElementsAfterMutationTimeout: number | NodeJS.Timeout; private readonly updateAfterMutationTimeoutDelay = 1000; @@ -70,6 +72,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.setupMutationObserver(); } + if (!this.intersectionObserver) { + this.setupIntersectionObserver(); + } + if (!this.domRecentlyMutated && this.noFieldsFound) { return this.getFormattedPageDetails({}, []); } @@ -360,11 +366,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte tagName: this.getAttributeLowerCase(element, "tagName"), }; + if (!autofillFieldBase.viewable) { + this.elementInitializingIntersectionObserver.add(element); + this.intersectionObserver.observe(element); + } + if (elementIsSpanElement(element)) { this.cacheAutofillFieldElement(index, element, autofillFieldBase); - // 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.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( + void this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( element, autofillFieldBase, ); @@ -407,9 +416,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte }; this.cacheAutofillFieldElement(index, element, autofillField); - // 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.autofillOverlayContentService?.setupAutofillOverlayListenerOnField(element, autofillField); + void this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( + element, + autofillField, + ); return autofillField; }; @@ -1189,8 +1199,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return; } - // 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.updateAutofillFieldElementData( attributeName, targetElement as ElementWithOpId<FormFieldElement>, @@ -1232,13 +1240,12 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte /** * Updates the autofill field element data based on the passed attribute name. + * * @param {string} attributeName * @param {ElementWithOpId<FormFieldElement>} element * @param {AutofillField} dataTarget - * @returns {Promise<void>} - * @private */ - private async updateAutofillFieldElementData( + private updateAutofillFieldElementData( attributeName: string, element: ElementWithOpId<FormFieldElement>, dataTarget: AutofillField, @@ -1304,6 +1311,52 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return attributeValue; } + /** + * Sets up an IntersectionObserver to observe found form + * field elements that are not viewable in the viewport. + */ + private setupIntersectionObserver() { + this.intersectionObserver = new IntersectionObserver(this.handleFormElementIntersection, { + root: null, + rootMargin: "0px", + threshold: 1.0, + }); + } + + /** + * Handles observed form field elements that are not viewable in the viewport. + * Will re-evaluate the visibility of the element and set up the autofill + * overlay listeners on the field if it is viewable. + * + * @param entries - The entries observed by the IntersectionObserver + */ + private handleFormElementIntersection = async (entries: IntersectionObserverEntry[]) => { + for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) { + const entry = entries[entryIndex]; + const formFieldElement = entry.target as ElementWithOpId<FormFieldElement>; + if (this.elementInitializingIntersectionObserver.has(formFieldElement)) { + this.elementInitializingIntersectionObserver.delete(formFieldElement); + continue; + } + + const isViewable = + await this.domElementVisibilityService.isFormFieldViewable(formFieldElement); + if (!isViewable) { + continue; + } + + const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); + cachedAutofillFieldElement.viewable = true; + + void this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( + formFieldElement, + cachedAutofillFieldElement, + ); + + this.intersectionObserver.unobserve(entry.target); + } + }; + /** * Destroys the CollectAutofillContentService. Clears all * timeouts and disconnects the mutation observer. @@ -1313,6 +1366,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte clearTimeout(this.updateAutofillElementsAfterMutationTimeout); } this.mutationObserver?.disconnect(); + this.intersectionObserver?.disconnect(); } } From 2e51d96416bc119b33e6eafa89485f2c9a108327 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:00:27 -0700 Subject: [PATCH 51/51] [PM-5264] Implement StateProvider in LoginEmailService (#7662) * setup StateProvider in LoginService * replace implementations * replace implementation * remove stateService * change storage location for web to 'disk-local' * implement migrate() method of Migrator * add RememberedEmailMigrator to migrate.ts * add rollback * add tests * replace implementation * replace implementation * add StateProvider to Desktop services * rename LoginService to RememberEmailService * update state definition * rename file * rename to storedEmail * rename service to EmailService to avoid confusion * add jsdocs * refactor login.component.ts * fix typos * fix test * rename to LoginEmailService * update factory * more renaming * remove duplicate logic and rename method * convert storedEmail to observable * refactor to remove setStoredEmail() method * move service to libs/auth/common * address floating promises * remove comment * remove unnecessary deps in service registration --- .../login-email-service.factory.ts | 28 +++++++ apps/browser/src/auth/popup/hint.component.ts | 6 +- .../src/auth/popup/home.component.html | 2 +- apps/browser/src/auth/popup/home.component.ts | 47 +++++------ .../popup/login-via-auth-request.component.ts | 6 +- .../src/auth/popup/login.component.html | 2 +- .../browser/src/auth/popup/login.component.ts | 16 ++-- .../src/auth/popup/two-factor.component.ts | 6 +- .../browser/src/background/main.background.ts | 6 +- .../src/popup/services/services.module.ts | 7 -- .../app/layout/account-switcher.component.ts | 7 +- .../src/app/services/services.module.ts | 7 -- apps/desktop/src/auth/hint.component.ts | 6 +- .../login/login-via-auth-request.component.ts | 6 +- .../src/auth/login/login.component.html | 4 +- .../desktop/src/auth/login/login.component.ts | 10 ++- apps/desktop/src/auth/two-factor.component.ts | 6 +- apps/web/src/app/auth/hint.component.ts | 6 +- .../src/app/auth/login/login.component.html | 4 +- .../web/src/app/auth/login/login.component.ts | 23 ++---- apps/web/src/app/auth/two-factor.component.ts | 8 +- apps/web/src/app/core/core.module.ts | 8 +- ...base-login-decryption-options.component.ts | 20 ++--- .../src/auth/components/hint.component.ts | 6 +- .../login-via-auth-request.component.ts | 17 ++-- .../src/auth/components/login.component.ts | 47 ++++++----- .../components/two-factor.component.spec.ts | 16 ++-- .../auth/components/two-factor.component.ts | 6 +- .../src/services/jslib-services.module.ts | 10 +-- libs/auth/src/common/abstractions/index.ts | 1 + .../abstractions/login-email.service.ts | 38 +++++++++ libs/auth/src/common/services/index.ts | 1 + .../login-email/login-email.service.ts | 52 ++++++++++++ .../src/auth/abstractions/login.service.ts | 8 -- .../common/src/auth/services/login.service.ts | 35 -------- .../platform/abstractions/state.service.ts | 2 - .../platform/models/domain/global-state.ts | 1 - .../src/platform/services/state.service.ts | 17 ---- .../src/platform/state/state-definitions.ts | 5 +- libs/common/src/state-migrations/migrate.ts | 6 +- ...emembered-email-to-state-providers.spec.ts | 81 +++++++++++++++++++ ...ove-remembered-email-to-state-providers.ts | 46 +++++++++++ 42 files changed, 396 insertions(+), 240 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/login-email-service.factory.ts create mode 100644 libs/auth/src/common/abstractions/login-email.service.ts create mode 100644 libs/auth/src/common/services/login-email/login-email.service.ts delete mode 100644 libs/common/src/auth/abstractions/login.service.ts delete mode 100644 libs/common/src/auth/services/login.service.ts create mode 100644 libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts diff --git a/apps/browser/src/auth/background/service-factories/login-email-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-email-service.factory.ts new file mode 100644 index 000000000000..6e98a9a88605 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/login-email-service.factory.ts @@ -0,0 +1,28 @@ +import { LoginEmailServiceAbstraction, LoginEmailService } from "@bitwarden/auth/common"; + +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type LoginEmailServiceFactoryOptions = FactoryOptions; + +export type LoginEmailServiceInitOptions = LoginEmailServiceFactoryOptions & + StateProviderInitOptions; + +export function loginEmailServiceFactory( + cache: { loginEmailService?: LoginEmailServiceAbstraction } & CachedServices, + opts: LoginEmailServiceInitOptions, +): Promise<LoginEmailServiceAbstraction> { + return factory( + cache, + "loginEmailService", + opts, + async () => new LoginEmailService(await stateProviderFactory(cache, opts)), + ); +} diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts index a1f79cd457a5..214a43efb717 100644 --- a/apps/browser/src/auth/popup/hint.component.ts +++ b/apps/browser/src/auth/popup/hint.component.ts @@ -2,8 +2,8 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { HintComponent as BaseHintComponent } from "@bitwarden/angular/auth/components/hint.component"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -20,9 +20,9 @@ export class HintComponent extends BaseHintComponent { apiService: ApiService, logService: LogService, private route: ActivatedRoute, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginService); + super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); super.onSuccessfulSubmit = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/auth/popup/home.component.html b/apps/browser/src/auth/popup/home.component.html index f70a4c6d0307..8e23d96c49d1 100644 --- a/apps/browser/src/auth/popup/home.component.html +++ b/apps/browser/src/auth/popup/home.component.html @@ -30,7 +30,7 @@ </form> <p class="createAccountLink"> {{ "newAroundHere" | i18n }} - <a routerLink="/register" (click)="setFormValues()">{{ "createAccount" | i18n }}</a> + <a routerLink="/register" (click)="setLoginEmailValues()">{{ "createAccount" | i18n }}</a> </p> </div> </div> diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 1360e6c8a6a0..db83736be8a6 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -1,14 +1,13 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AccountSwitcherService } from "./account-switching/services/account-switcher.service"; @@ -29,38 +28,32 @@ export class HomeComponent implements OnInit, OnDestroy { constructor( protected platformUtilsService: PlatformUtilsService, - private stateService: StateService, private formBuilder: FormBuilder, private router: Router, private i18nService: I18nService, private environmentService: EnvironmentService, - private loginService: LoginService, + private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, ) {} async ngOnInit(): Promise<void> { - let savedEmail = this.loginService.getEmail(); - const rememberEmail = this.loginService.getRememberEmail(); + const email = this.loginEmailService.getEmail(); + const rememberEmail = this.loginEmailService.getRememberEmail(); - if (savedEmail != null) { - this.formGroup.patchValue({ - email: savedEmail, - rememberEmail: rememberEmail, - }); + if (email != null) { + this.formGroup.patchValue({ email, rememberEmail }); } else { - savedEmail = await this.stateService.getRememberedEmail(); - if (savedEmail != null) { - this.formGroup.patchValue({ - email: savedEmail, - rememberEmail: true, - }); + const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); + + if (storedEmail != null) { + this.formGroup.patchValue({ email: storedEmail, rememberEmail: true }); } } this.environmentSelector.onOpenSelfHostedSettings .pipe(takeUntil(this.destroyed$)) .subscribe(() => { - this.setFormValues(); + this.setLoginEmailValues(); // 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.router.navigate(["environment"]); @@ -76,8 +69,9 @@ export class HomeComponent implements OnInit, OnDestroy { return this.accountSwitcherService.availableAccounts$; } - submit() { + async submit() { this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { this.platformUtilsService.showToast( "error", @@ -87,15 +81,12 @@ export class HomeComponent implements OnInit, OnDestroy { return; } - this.loginService.setEmail(this.formGroup.value.email); - this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); - // 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.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); + this.setLoginEmailValues(); + await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); } - setFormValues() { - this.loginService.setEmail(this.formGroup.value.email); - this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); + setLoginEmailValues() { + this.loginEmailService.setEmail(this.formGroup.value.email); + this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); } } diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index 4ef1c78cb49d..8d438d5b7862 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -6,12 +6,12 @@ import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@b import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -44,7 +44,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { anonymousHubService: AnonymousHubService, validationService: ValidationService, stateService: StateService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, syncService: SyncService, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, @@ -66,7 +66,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { anonymousHubService, validationService, stateService, - loginService, + loginEmailService, deviceTrustCryptoService, authRequestService, loginStrategyService, diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login.component.html index f6ebb747f777..b24a25a0f1af 100644 --- a/apps/browser/src/auth/popup/login.component.html +++ b/apps/browser/src/auth/popup/login.component.html @@ -52,7 +52,7 @@ <h1 class="login-center"> </div> </div> <div class="box-footer"> - <button type="button" class="btn link" routerLink="/hint" (click)="setFormValues()"> + <button type="button" class="btn link" routerLink="/hint" (click)="setLoginEmailValues()"> <b>{{ "getMasterPasswordHint" | i18n }}</b> </button> </div> diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 5c302455e66d..ff0ee8a392d1 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -5,9 +5,11 @@ import { firstValueFrom } from "rxjs"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, +} from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -46,7 +48,7 @@ export class LoginComponent extends BaseLoginComponent { formBuilder: FormBuilder, formValidationErrorService: FormValidationErrorsService, route: ActivatedRoute, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -66,7 +68,7 @@ export class LoginComponent extends BaseLoginComponent { formBuilder, formValidationErrorService, route, - loginService, + loginEmailService, ssoLoginService, webAuthnLoginService, ); @@ -77,8 +79,8 @@ export class LoginComponent extends BaseLoginComponent { this.showPasswordless = flagEnabled("showPasswordless"); if (this.showPasswordless) { - this.formGroup.controls.email.setValue(this.loginService.getEmail()); - this.formGroup.controls.rememberEmail.setValue(this.loginService.getRememberEmail()); + this.formGroup.controls.email.setValue(this.loginEmailService.getEmail()); + this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail()); // 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.validateEmail(); @@ -94,7 +96,7 @@ export class LoginComponent extends BaseLoginComponent { async launchSsoBrowser() { // Save off email for SSO await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); - await this.loginService.saveEmailSettings(); + await this.loginEmailService.saveEmailSettings(); // Generate necessary sso params const passwordOptions: any = { type: "password", diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 94dfb5155b14..dd541f63f820 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -7,10 +7,10 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -57,7 +57,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, @@ -78,7 +78,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 957ebd998b36..ee17a7f1f06a 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -9,6 +9,7 @@ import { UserDecryptionOptionsService, AuthRequestServiceAbstraction, AuthRequestService, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -258,6 +259,7 @@ export default class MainBackground { auditService: AuditServiceAbstraction; authService: AuthServiceAbstraction; loginStrategyService: LoginStrategyServiceAbstraction; + loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; exportService: VaultExportServiceAbstraction; @@ -1080,7 +1082,9 @@ export default class MainBackground { await this.stateService.setActiveUser(userId); if (userId == null) { - await this.stateService.setRememberedEmail(null); + this.loginEmailService.setRememberEmail(false); + await this.loginEmailService.saveEmailSettings(); + await this.refreshBadge(); await this.refreshMenu(); await this.overlayBackground.updateOverlayCiphers(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 9bdd31785480..fbeabca4621c 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -30,13 +30,11 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { AutofillSettingsService, AutofillSettingsServiceAbstraction, @@ -429,11 +427,6 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserFileDownloadService, deps: [], }), - safeProvider({ - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }), safeProvider({ provide: SYSTEM_THEME_OBSERVABLE, useFactory: (platformUtilsService: PlatformUtilsService) => { diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 499300086dbb..4e39ab002922 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -4,6 +4,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -91,6 +92,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private router: Router, private tokenService: TokenService, private environmentService: EnvironmentService, + private loginEmailService: LoginEmailServiceAbstraction, ) {} async ngOnInit(): Promise<void> { @@ -137,7 +139,10 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { async addAccount() { this.close(); - await this.stateService.setRememberedEmail(null); + + this.loginEmailService.setRememberEmail(false); + await this.loginEmailService.saveEmailSettings(); + await this.router.navigate(["/login"]); await this.stateService.setActiveUser(null); } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 1d75ff4ca90e..84932ce7d95f 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -20,9 +20,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -221,11 +219,6 @@ const safeProviders: SafeProvider[] = [ DesktopAutofillSettingsService, ], }), - safeProvider({ - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }), safeProvider({ provide: CryptoFunctionServiceAbstraction, useClass: RendererCryptoFunctionService, diff --git a/apps/desktop/src/auth/hint.component.ts b/apps/desktop/src/auth/hint.component.ts index 5eeeb8106e9b..cee1f189817d 100644 --- a/apps/desktop/src/auth/hint.component.ts +++ b/apps/desktop/src/auth/hint.component.ts @@ -2,8 +2,8 @@ import { Component } from "@angular/core"; import { Router } from "@angular/router"; import { HintComponent as BaseHintComponent } from "@bitwarden/angular/auth/components/hint.component"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -19,8 +19,8 @@ export class HintComponent extends BaseHintComponent { i18nService: I18nService, apiService: ApiService, logService: LogService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginService); + super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); } } diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index 9a6fa8e38828..28163d09d090 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -7,12 +7,12 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -53,7 +53,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { private modalService: ModalService, syncService: SyncService, stateService: StateService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, @@ -74,7 +74,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { anonymousHubService, validationService, stateService, - loginService, + loginEmailService, deviceTrustCryptoService, authRequestService, loginStrategyService, diff --git a/apps/desktop/src/auth/login/login.component.html b/apps/desktop/src/auth/login/login.component.html index 06ee5db32dc0..eef0580d4e1d 100644 --- a/apps/desktop/src/auth/login/login.component.html +++ b/apps/desktop/src/auth/login/login.component.html @@ -99,7 +99,7 @@ class="btn block" type="button" routerLink="/accessibility-cookie" - (click)="setFormValues()" + (click)="setLoginEmailValues()" > <i class="bwi bwi-universal-access" aria-hidden="true"></i> {{ "loadAccessibilityCookie" | i18n }} @@ -139,7 +139,7 @@ type="button" class="text text-primary password-hint-btn" routerLink="/hint" - (click)="setFormValues()" + (click)="setLoginEmailValues()" > {{ "getMasterPasswordHint" | i18n }} </button> diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index dd22a0fa373e..eb7b92436248 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -7,9 +7,11 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, +} from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -69,7 +71,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { formBuilder: FormBuilder, formValidationErrorService: FormValidationErrorsService, route: ActivatedRoute, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -89,7 +91,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { formBuilder, formValidationErrorService, route, - loginService, + loginEmailService, ssoLoginService, webAuthnLoginService, ); diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index 8b46f3d1b9af..fdbc52b4bf4c 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -7,10 +7,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -56,7 +56,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, @@ -75,7 +75,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, diff --git a/apps/web/src/app/auth/hint.component.ts b/apps/web/src/app/auth/hint.component.ts index d3a7c0043138..116b0f3f830d 100644 --- a/apps/web/src/app/auth/hint.component.ts +++ b/apps/web/src/app/auth/hint.component.ts @@ -2,8 +2,8 @@ import { Component } from "@angular/core"; import { Router } from "@angular/router"; import { HintComponent as BaseHintComponent } from "@bitwarden/angular/auth/components/hint.component"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -19,8 +19,8 @@ export class HintComponent extends BaseHintComponent { apiService: ApiService, platformUtilsService: PlatformUtilsService, logService: LogService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginService); + super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); } } diff --git a/apps/web/src/app/auth/login/login.component.html b/apps/web/src/app/auth/login/login.component.html index 5c68058a3cb9..0e29a3427861 100644 --- a/apps/web/src/app/auth/login/login.component.html +++ b/apps/web/src/app/auth/login/login.component.html @@ -1,6 +1,6 @@ <form #form - (ngSubmit)="submit()" + (ngSubmit)="submit(false)" [appApiAction]="formPromise" class="tw-container tw-mx-auto" [formGroup]="formGroup" @@ -91,7 +91,7 @@ class="-tw-mt-2" routerLink="/hint" (mousedown)="goToHint()" - (click)="setFormValues()" + (click)="setLoginEmailValues()" >{{ "getMasterPasswordHint" | i18n }}</a > </div> diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 1d2d1859e94e..9f628b9389e2 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -6,7 +6,10 @@ import { first } from "rxjs/operators"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, +} from "@bitwarden/auth/common"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; @@ -14,7 +17,6 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -62,7 +64,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { private routerService: RouterService, formBuilder: FormBuilder, formValidationErrorService: FormValidationErrorsService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -82,7 +84,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { formBuilder, formValidationErrorService, route, - loginService, + loginEmailService, ssoLoginService, webAuthnLoginService, ); @@ -173,14 +175,14 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { } } - this.loginService.clearValues(); + this.loginEmailService.clearValues(); // 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.router.navigate([this.successRoute]); } goToHint() { - this.setFormValues(); + this.setLoginEmailValues(); // 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.router.navigateByUrl("/hint"); @@ -201,15 +203,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { this.router.navigate(["/register"]); } - async submit() { - const rememberEmail = this.formGroup.value.rememberEmail; - - if (!rememberEmail) { - await this.stateService.setRememberedEmail(null); - } - await super.submit(false); - } - protected override handleMigrateEncryptionKey(result: AuthResult): boolean { if (!result.requiresEncryptionKeyMigration) { return false; diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 6760ab449faa..65bf1dba58aa 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -6,10 +6,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -46,7 +46,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, @@ -65,7 +65,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, @@ -103,7 +103,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest } goAfterLogIn = async () => { - this.loginService.clearValues(); + this.loginEmailService.clearValues(); // 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.router.navigate([this.successRoute], { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index e2d3f64f2d69..bd514b1d1802 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -16,8 +16,6 @@ import { import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -29,6 +27,7 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; +// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; @@ -117,11 +116,6 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service"; provide: FileDownloadService, useClass: WebFileDownloadService, }, - { - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateService], - }, CollectionAdminService, { provide: OBSERVABLE_DISK_LOCAL_STORAGE, diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 79202054c549..6bb545c4b537 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -15,6 +15,7 @@ import { } from "rxjs"; import { + LoginEmailServiceAbstraction, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; @@ -23,7 +24,6 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -82,7 +82,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected activatedRoute: ActivatedRoute, protected messagingService: MessagingService, protected tokenService: TokenService, - protected loginService: LoginService, + protected loginEmailService: LoginEmailServiceAbstraction, protected organizationApiService: OrganizationApiServiceAbstraction, protected cryptoService: CryptoService, protected organizationUserService: OrganizationUserService, @@ -244,23 +244,17 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { return; } - this.loginService.setEmail(this.data.userEmail); - // 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.router.navigate(["/login-with-device"]); + this.loginEmailService.setEmail(this.data.userEmail); + await this.router.navigate(["/login-with-device"]); } async requestAdminApproval() { - this.loginService.setEmail(this.data.userEmail); - // 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.router.navigate(["/admin-approval-requested"]); + this.loginEmailService.setEmail(this.data.userEmail); + await this.router.navigate(["/admin-approval-requested"]); } async approveWithMasterPassword() { - // 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.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } }); + await this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } }); } async createUser() { diff --git a/libs/angular/src/auth/components/hint.component.ts b/libs/angular/src/auth/components/hint.component.ts index 54edc5b8fafe..484604b6a5aa 100644 --- a/libs/angular/src/auth/components/hint.component.ts +++ b/libs/angular/src/auth/components/hint.component.ts @@ -1,8 +1,8 @@ import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -22,11 +22,11 @@ export class HintComponent implements OnInit { protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, private logService: LogService, - private loginService: LoginService, + private loginEmailService: LoginEmailServiceAbstraction, ) {} ngOnInit(): void { - this.email = this.loginService.getEmail() ?? ""; + this.email = this.loginEmailService.getEmail() ?? ""; } async submit() { diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index b1d0b81922b8..66b7c1918cdd 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -6,12 +6,12 @@ import { AuthRequestLoginCredentials, AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; @@ -83,7 +83,7 @@ export class LoginViaAuthRequestComponent private anonymousHubService: AnonymousHubService, private validationService: ValidationService, private stateService: StateService, - private loginService: LoginService, + private loginEmailService: LoginEmailServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, @@ -94,7 +94,7 @@ export class LoginViaAuthRequestComponent // Why would the existence of the email depend on the navigation? const navigation = this.router.getCurrentNavigation(); if (navigation) { - this.email = this.loginService.getEmail(); + this.email = this.loginEmailService.getEmail(); } // Gets signalR push notification @@ -151,7 +151,7 @@ export class LoginViaAuthRequestComponent } else { // Standard auth request // TODO: evaluate if we can remove the setting of this.email in the constructor - this.email = this.loginService.getEmail(); + this.email = this.loginEmailService.getEmail(); if (!this.email) { this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); @@ -472,17 +472,10 @@ export class LoginViaAuthRequestComponent } } - async setRememberEmailValues() { - const rememberEmail = this.loginService.getRememberEmail(); - const rememberedEmail = this.loginService.getEmail(); - await this.stateService.setRememberedEmail(rememberEmail ? rememberedEmail : null); - this.loginService.clearValues(); - } - private async handleSuccessfulLoginNavigation() { if (this.state === State.StandardAuthRequest) { // Only need to set remembered email on standard login with auth req flow - await this.setRememberEmailValues(); + await this.loginEmailService.saveEmailSettings(); } if (this.onSuccessfulLogin != null) { diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 217d33119887..bcdf747406ec 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -4,9 +4,12 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Subject, firstValueFrom } from "rxjs"; import { take, takeUntil } from "rxjs/operators"; -import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, + PasswordLoginCredentials, +} from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -77,7 +80,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, protected formBuilder: FormBuilder, protected formValidationErrorService: FormValidationErrorsService, protected route: ActivatedRoute, - protected loginService: LoginService, + protected loginEmailService: LoginEmailServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -93,25 +96,23 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, const queryParamsEmail = params.email; if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) { - this.formGroup.get("email").setValue(queryParamsEmail); - this.loginService.setEmail(queryParamsEmail); + this.formGroup.controls.email.setValue(queryParamsEmail); this.paramEmailSet = true; } }); - let email = this.loginService.getEmail(); - - if (email == null || email === "") { - email = await this.stateService.getRememberedEmail(); - } if (!this.paramEmailSet) { - this.formGroup.get("email")?.setValue(email ?? ""); + const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); + this.formGroup.controls.email.setValue(storedEmail ?? ""); } - let rememberEmail = this.loginService.getRememberEmail(); + + let rememberEmail = this.loginEmailService.getRememberEmail(); + if (rememberEmail == null) { - rememberEmail = (await this.stateService.getRememberedEmail()) != null; + rememberEmail = (await firstValueFrom(this.loginEmailService.storedEmail$)) != null; } - this.formGroup.get("rememberEmail")?.setValue(rememberEmail); + + this.formGroup.controls.rememberEmail.setValue(rememberEmail); } ngOnDestroy() { @@ -148,8 +149,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, this.formPromise = this.loginStrategyService.logIn(credentials); const response = await this.formPromise; - this.setFormValues(); - await this.loginService.saveEmailSettings(); + + this.setLoginEmailValues(); + await this.loginEmailService.saveEmailSettings(); + if (this.handleCaptchaRequired(response)) { return; } else if (this.handleMigrateEncryptionKey(response)) { @@ -214,7 +217,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, return; } - this.setFormValues(); + this.setLoginEmailValues(); // 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.router.navigate(["/login-with-device"]); @@ -292,14 +295,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, } } - setFormValues() { - this.loginService.setEmail(this.formGroup.value.email); - this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); + setLoginEmailValues() { + this.loginEmailService.setEmail(this.formGroup.value.email); + this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); } async saveEmailSettings() { - this.setFormValues(); - await this.loginService.saveEmailSettings(); + this.setLoginEmailValues(); + await this.loginEmailService.saveEmailSettings(); // Save off email for SSO await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 9703c7e7030d..bff39188ea98 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -7,14 +7,14 @@ import { BehaviorSubject } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { - FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, + FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption, FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption, FakeUserDecryptionOptions as UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -59,7 +59,7 @@ describe("TwoFactorComponent", () => { let mockLogService: MockProxy<LogService>; let mockTwoFactorService: MockProxy<TwoFactorService>; let mockAppIdService: MockProxy<AppIdService>; - let mockLoginService: MockProxy<LoginService>; + let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>; let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; @@ -89,7 +89,7 @@ describe("TwoFactorComponent", () => { mockLogService = mock<LogService>(); mockTwoFactorService = mock<TwoFactorService>(); mockAppIdService = mock<AppIdService>(); - mockLoginService = mock<LoginService>(); + mockLoginEmailService = mock<LoginEmailServiceAbstraction>(); mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); mockSsoLoginService = mock<SsoLoginServiceAbstraction>(); mockConfigService = mock<ConfigService>(); @@ -163,7 +163,7 @@ describe("TwoFactorComponent", () => { { provide: LogService, useValue: mockLogService }, { provide: TwoFactorService, useValue: mockTwoFactorService }, { provide: AppIdService, useValue: mockAppIdService }, - { provide: LoginService, useValue: mockLoginService }, + { provide: LoginEmailServiceAbstraction, useValue: mockLoginEmailService }, { provide: UserDecryptionOptionsServiceAbstraction, useValue: mockUserDecryptionOptionsService, @@ -280,11 +280,11 @@ describe("TwoFactorComponent", () => { expect(component.onSuccessfulLogin).toHaveBeenCalled(); }); - it("calls loginService.clearValues() when login is successful", async () => { + it("calls loginEmailService.clearValues() when login is successful", async () => { // Arrange mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); - // spy on loginService.clearValues - const clearValuesSpy = jest.spyOn(mockLoginService, "clearValues"); + // spy on loginEmailService.clearValues + const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues"); // Act await component.doSubmit(); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index f64e591fa267..c306e6cc8046 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -8,12 +8,12 @@ import { first } from "rxjs/operators"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, TrustedDeviceUserDecryptionOption, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -88,7 +88,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected logService: LogService, protected twoFactorService: TwoFactorService, protected appIdService: AppIdService, - protected loginService: LoginService, + protected loginEmailService: LoginEmailServiceAbstraction, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected configService: ConfigService, @@ -288,7 +288,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI // - TDE login decryption options component // - Browser SSO on extension open await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier); - this.loginService.clearValues(); + this.loginEmailService.clearValues(); // note: this flow affects both TDE & standard users if (this.isForcePasswordResetRequired(authResult)) { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 841edb4289a2..a31d5141c472 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -7,6 +7,8 @@ import { PinCryptoService, LoginStrategyServiceAbstraction, LoginStrategyService, + LoginEmailServiceAbstraction, + LoginEmailService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, @@ -58,7 +60,6 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; @@ -77,7 +78,6 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -874,9 +874,9 @@ const safeProviders: SafeProvider[] = [ deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], }), safeProvider({ - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], + provide: LoginEmailServiceAbstraction, + useClass: LoginEmailService, + deps: [StateProvider], }), safeProvider({ provide: OrgDomainInternalServiceAbstraction, diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts index 1feee6695a96..71280b72f63b 100644 --- a/libs/auth/src/common/abstractions/index.ts +++ b/libs/auth/src/common/abstractions/index.ts @@ -1,4 +1,5 @@ export * from "./pin-crypto.service.abstraction"; +export * from "./login-email.service"; export * from "./login-strategy.service"; export * from "./user-decryption-options.service.abstraction"; export * from "./auth-request.service.abstraction"; diff --git a/libs/auth/src/common/abstractions/login-email.service.ts b/libs/auth/src/common/abstractions/login-email.service.ts new file mode 100644 index 000000000000..89165af54311 --- /dev/null +++ b/libs/auth/src/common/abstractions/login-email.service.ts @@ -0,0 +1,38 @@ +import { Observable } from "rxjs"; + +export abstract class LoginEmailServiceAbstraction { + /** + * An observable that monitors the storedEmail + */ + storedEmail$: Observable<string>; + /** + * Gets the current email being used in the login process. + * @returns A string of the email. + */ + getEmail: () => string; + /** + * Sets the current email being used in the login process. + * @param email The email to be set. + */ + setEmail: (email: string) => void; + /** + * Gets whether or not the email should be stored on disk. + * @returns A boolean stating whether or not the email should be stored on disk. + */ + getRememberEmail: () => boolean; + /** + * Sets whether or not the email should be stored on disk. + */ + setRememberEmail: (value: boolean) => void; + /** + * Sets the email and rememberEmail properties to null. + */ + clearValues: () => void; + /** + * - If rememberEmail is true, sets the storedEmail on disk to the current email. + * - If rememberEmail is false, sets the storedEmail on disk to null. + * - Then sets the email and rememberEmail properties to null. + * @returns A promise that resolves once the email settings are saved. + */ + saveEmailSettings: () => Promise<void>; +} diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index 12215cf6b4d2..5a0fc083dd75 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -1,4 +1,5 @@ export * from "./pin-crypto/pin-crypto.service.implementation"; +export * from "./login-email/login-email.service"; export * from "./login-strategies/login-strategy.service"; export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts new file mode 100644 index 000000000000..171af07430e8 --- /dev/null +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -0,0 +1,52 @@ +import { Observable } from "rxjs"; + +import { + GlobalState, + KeyDefinition, + LOGIN_EMAIL_DISK, + StateProvider, +} from "../../../../../common/src/platform/state"; +import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service"; + +const STORED_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_DISK, "storedEmail", { + deserializer: (value: string) => value, +}); + +export class LoginEmailService implements LoginEmailServiceAbstraction { + private email: string; + private rememberEmail: boolean; + + private readonly storedEmailState: GlobalState<string>; + storedEmail$: Observable<string>; + + constructor(private stateProvider: StateProvider) { + this.storedEmailState = this.stateProvider.getGlobal(STORED_EMAIL); + this.storedEmail$ = this.storedEmailState.state$; + } + + getEmail() { + return this.email; + } + + setEmail(email: string) { + this.email = email; + } + + getRememberEmail() { + return this.rememberEmail; + } + + setRememberEmail(value: boolean) { + this.rememberEmail = value; + } + + clearValues() { + this.email = null; + this.rememberEmail = null; + } + + async saveEmailSettings() { + await this.storedEmailState.update(() => (this.rememberEmail ? this.email : null)); + this.clearValues(); + } +} diff --git a/libs/common/src/auth/abstractions/login.service.ts b/libs/common/src/auth/abstractions/login.service.ts deleted file mode 100644 index 9a884fd5d1ca..000000000000 --- a/libs/common/src/auth/abstractions/login.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -export abstract class LoginService { - getEmail: () => string; - getRememberEmail: () => boolean; - setEmail: (value: string) => void; - setRememberEmail: (value: boolean) => void; - clearValues: () => void; - saveEmailSettings: () => Promise<void>; -} diff --git a/libs/common/src/auth/services/login.service.ts b/libs/common/src/auth/services/login.service.ts deleted file mode 100644 index f1d038b2f808..000000000000 --- a/libs/common/src/auth/services/login.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { StateService } from "../../platform/abstractions/state.service"; -import { LoginService as LoginServiceAbstraction } from "../abstractions/login.service"; - -export class LoginService implements LoginServiceAbstraction { - private _email: string; - private _rememberEmail: boolean; - - constructor(private stateService: StateService) {} - - getEmail() { - return this._email; - } - - getRememberEmail() { - return this._rememberEmail; - } - - setEmail(value: string) { - this._email = value; - } - - setRememberEmail(value: boolean) { - this._rememberEmail = value; - } - - clearValues() { - this._email = null; - this._rememberEmail = null; - } - - async saveEmailSettings() { - await this.stateService.setRememberedEmail(this._rememberEmail ? this._email : null); - this.clearValues(); - } -} diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 0ca0615380e1..79dc83868e4a 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -262,8 +262,6 @@ export abstract class StateService<T extends Account = Account> { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>; - getRememberedEmail: (options?: StorageOptions) => Promise<string>; - setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>; getSecurityStamp: (options?: StorageOptions) => Promise<string>; setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; getUserId: (options?: StorageOptions) => Promise<string>; diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 7e35606e261b..b0a59e4617fe 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -3,7 +3,6 @@ import { ThemeType } from "../../enums"; export class GlobalState { installedVersion?: string; organizationInvitation?: any; - rememberedEmail?: string; theme?: ThemeType = ThemeType.System; twoFactorToken?: string; biometricFingerprintValidated?: boolean; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index d4297ecf94ef..c0b2a8fa2e7f 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1241,23 +1241,6 @@ export class StateService< ); } - async getRememberedEmail(options?: StorageOptions): Promise<string> { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.rememberedEmail; - } - - async setRememberedEmail(value: string, options?: StorageOptions): Promise<void> { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.rememberedEmail = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - async getSecurityStamp(options?: StorageOptions): Promise<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 35714ee7c4a1..814bf0280f0e 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -38,13 +38,16 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); +export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { + web: "disk-local", +}); +export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); export const SSO_DISK = new StateDefinition("ssoLogin", "disk"); export const TOKEN_DISK = new StateDefinition("token", "disk"); export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { web: "disk-local", }); export const TOKEN_MEMORY = new StateDefinition("token", "memory"); -export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); // Autofill diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 60bd31d0498d..5222ee7ad7dd 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -47,6 +47,7 @@ import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-stat import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider"; +import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; @@ -54,7 +55,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 50; +export const CURRENT_VERSION = 51; export type MinVersion = typeof MIN_VERSION; @@ -107,7 +108,8 @@ export function createMigrationBuilder() { .with(MoveDesktopSettingsMigrator, 46, 47) .with(MoveDdgToStateProviderMigrator, 47, 48) .with(AccountServerConfigMigrator, 48, 49) - .with(KeyConnectorMigrator, 49, CURRENT_VERSION); + .with(KeyConnectorMigrator, 49, 50) + .with(RememberedEmailMigrator, 50, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts new file mode 100644 index 000000000000..f36b5842aad0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts @@ -0,0 +1,81 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper, runMigrator } from "../migration-helper.spec"; + +import { RememberedEmailMigrator } from "./51-move-remembered-email-to-state-providers"; + +function rollbackJSON() { + return { + global: { + extra: "data", + }, + global_loginEmail_storedEmail: "user@example.com", + }; +} + +describe("RememberedEmailMigrator", () => { + const migrator = new RememberedEmailMigrator(50, 51); + + describe("migrate", () => { + it("should migrate the rememberedEmail property from the legacy global object to a global StorageKey as 'global_loginEmail_storedEmail'", async () => { + const output = await runMigrator(migrator, { + global: { + rememberedEmail: "user@example.com", + extra: "data", // Represents a global property that should persist after migration + }, + }); + + expect(output).toEqual({ + global: { + extra: "data", + }, + global_loginEmail_storedEmail: "user@example.com", + }); + }); + + it("should remove the rememberedEmail property from the legacy global object", async () => { + const output = await runMigrator(migrator, { + global: { + rememberedEmail: "user@example.com", + }, + }); + + expect(output.global).not.toHaveProperty("rememberedEmail"); + }); + }); + + describe("rollback", () => { + let helper: MockProxy<MigrationHelper>; + let sut: RememberedEmailMigrator; + + const keyDefinitionLike = { + key: "storedEmail", + stateDefinition: { + name: "loginEmail", + }, + }; + + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 51); + sut = new RememberedEmailMigrator(50, 51); + }); + + it("should null out the storedEmail global StorageKey", async () => { + await sut.rollback(helper); + + expect(helper.setToGlobal).toHaveBeenCalledTimes(1); + expect(helper.setToGlobal).toHaveBeenCalledWith(keyDefinitionLike, null); + }); + + it("should add the rememberedEmail property back to legacy global object", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + rememberedEmail: "user@example.com", + extra: "data", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts new file mode 100644 index 000000000000..b2b081871967 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts @@ -0,0 +1,46 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedGlobalState = { rememberedEmail?: string }; + +const LOGIN_EMAIL_STATE: StateDefinitionLike = { name: "loginEmail" }; + +const STORED_EMAIL: KeyDefinitionLike = { + key: "storedEmail", + stateDefinition: LOGIN_EMAIL_STATE, +}; + +export class RememberedEmailMigrator extends Migrator<50, 51> { + async migrate(helper: MigrationHelper): Promise<void> { + const legacyGlobal = await helper.get<ExpectedGlobalState>("global"); + + // Move global data + if (legacyGlobal?.rememberedEmail != null) { + await helper.setToGlobal(STORED_EMAIL, legacyGlobal.rememberedEmail); + } + + // Delete legacy global data + delete legacyGlobal?.rememberedEmail; + await helper.set("global", legacyGlobal); + } + + async rollback(helper: MigrationHelper): Promise<void> { + let legacyGlobal = await helper.get<ExpectedGlobalState>("global"); + let updatedLegacyGlobal = false; + const globalStoredEmail = await helper.getFromGlobal<string>(STORED_EMAIL); + + if (globalStoredEmail) { + if (!legacyGlobal) { + legacyGlobal = {}; + } + + updatedLegacyGlobal = true; + legacyGlobal.rememberedEmail = globalStoredEmail; + await helper.setToGlobal(STORED_EMAIL, null); + } + + if (updatedLegacyGlobal) { + await helper.set("global", legacyGlobal); + } + } +}