diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 8ac65d257c6..e82b490f813 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -174,20 +174,21 @@ jobs: with: path: | apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* ${{ env.RUNNER_TEMP }}/.cargo/registry ${{ env.RUNNER_TEMP }}/.cargo/git key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi + working-directory: apps/desktop/desktop_native env: PKG_CONFIG_ALLOW_CROSS: true PKG_CONFIG_ALL_STATIC: true TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - npm run build:cross-platform + node build.js cross-platform - name: Build application run: npm run dist:lin @@ -301,13 +302,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build & Sign (dev) env: @@ -584,13 +587,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build application (dev) run: npm run build @@ -748,13 +753,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -965,13 +972,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1168,13 +1177,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index b6f24bf9ae3..6c428c43d26 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -2,7 +2,7 @@ "devFlags": {}, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true, + "enableCipherKeyEncryption": false, "accountSwitching": false } } diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 950c5372d8f..e0925ebecc9 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -7,7 +7,7 @@ }, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true, + "enableCipherKeyEncryption": false, "accountSwitching": true } } diff --git a/apps/browser/config/production.json b/apps/browser/config/production.json index 64c6cb92a3b..027003f6c75 100644 --- a/apps/browser/config/production.json +++ b/apps/browser/config/production.json @@ -1,6 +1,6 @@ { "flags": { - "enableCipherKeyEncryption": true, + "enableCipherKeyEncryption": false, "accountSwitching": true } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 89b605ab632..3aa1ac097ce 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3477,7 +3477,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3489,6 +3489,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3498,9 +3501,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index ccc743d86d4..fb1b09de49c 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -94,7 +94,7 @@ {{ "awaitDesktop" | i18n }}

- + diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index a209523dc7c..0c626c68794 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -139,7 +139,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), unlockCompleted: ({ message }) => this.unlockCompleted(message), - doFullSync: () => this.updateOverlayCiphers(true), + doFullSync: () => this.updateOverlayCiphers(), addedCipher: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(), @@ -272,7 +272,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); } - if (!currentTab) { + if (!currentTab || !currentTab.url?.startsWith("http")) { + if (updateAllCipherTypes) { + this.cardAndIdentityCiphers = null; + } return; } diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts index d53d9e685ed..f5f8dd770c7 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts @@ -4,12 +4,21 @@ describe("FIDO2 page-script for manifest v2", () => { let createdScriptElement: HTMLScriptElement; jest.spyOn(window.document, "createElement"); + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true }); jest.clearAllMocks(); + jest.clearAllTimers(); jest.resetModules(); }); + afterAll(() => { + jest.useRealTimers(); + }); + it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => { Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true }); @@ -19,7 +28,7 @@ describe("FIDO2 page-script for manifest v2", () => { }); it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => { - jest.spyOn(window.document.head, "insertBefore").mockImplementation((node) => { + jest.spyOn(window.document.head, "prepend").mockImplementation((node) => { createdScriptElement = node as HTMLScriptElement; return node; }); @@ -28,16 +37,13 @@ describe("FIDO2 page-script for manifest v2", () => { expect(window.document.createElement).toHaveBeenCalledWith("script"); expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); - expect(window.document.head.insertBefore).toHaveBeenCalledWith( - expect.any(HTMLScriptElement), - window.document.head.firstChild, - ); + expect(window.document.head.prepend).toHaveBeenCalledWith(expect.any(HTMLScriptElement)); expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); }); it("appends the `page-script.js` file to the document element if the head is not available", () => { window.document.documentElement.removeChild(window.document.head); - jest.spyOn(window.document.documentElement, "insertBefore").mockImplementation((node) => { + jest.spyOn(window.document.documentElement, "prepend").mockImplementation((node) => { createdScriptElement = node as HTMLScriptElement; return node; }); @@ -46,9 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => { expect(window.document.createElement).toHaveBeenCalledWith("script"); expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); - expect(window.document.documentElement.insertBefore).toHaveBeenCalledWith( + expect(window.document.documentElement.prepend).toHaveBeenCalledWith( expect.any(HTMLScriptElement), - window.document.documentElement.firstChild, ); expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); }); @@ -63,6 +68,7 @@ describe("FIDO2 page-script for manifest v2", () => { jest.spyOn(createdScriptElement, "remove"); createdScriptElement.dispatchEvent(new Event("load")); + jest.runAllTimers(); expect(createdScriptElement.remove).toHaveBeenCalled(); }); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts index 4e806d29908..e5280c088bc 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts @@ -2,18 +2,20 @@ * This script handles injection of the FIDO2 override page script into the document. * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. */ -import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; - (function (globalContext) { if (globalContext.document.contentType !== "text/html") { return; } const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript); - script.addEventListener("load", () => script.remove()); + script.src = chrome.runtime.getURL("content/fido2-page-script.js"); + script.addEventListener("load", removeScriptOnLoad); const scriptInsertionPoint = globalContext.document.head || globalContext.document.documentElement; - scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild); + scriptInsertionPoint.prepend(script); + + function removeScriptOnLoad() { + globalThis.setTimeout(() => script?.remove(), 5000); + } })(globalThis); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts index 4afeb76a0d3..c75a37c1b65 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts @@ -2,26 +2,35 @@ * This script handles injection of the FIDO2 override page script into the document. * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. */ -import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; - (function (globalContext) { if (globalContext.document.contentType !== "text/html") { return; } - if (globalContext.document.readyState === "complete") { - loadScript(); + const script = globalContext.document.createElement("script"); + script.src = chrome.runtime.getURL("content/fido2-page-script.js"); + script.addEventListener("load", removeScriptOnLoad); + + // We are ensuring that the script injection is delayed in the event that we are loading + // within an iframe element. This prevents an issue with web mail clients that load content + // using ajax within iframes. In particular, Zimbra web mail client was observed to have this issue. + // @see https://github.com/bitwarden/clients/issues/9618 + const delayScriptInjection = + globalContext.window.top !== globalContext.window && + globalContext.document.readyState !== "complete"; + if (delayScriptInjection) { + globalContext.document.addEventListener("DOMContentLoaded", injectScript); } else { - globalContext.addEventListener("DOMContentLoaded", loadScript); + injectScript(); } - function loadScript() { - const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript); - script.addEventListener("load", () => script.remove()); - + function injectScript() { const scriptInsertionPoint = globalContext.document.head || globalContext.document.documentElement; - scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild); + scriptInsertionPoint.prepend(script); + } + + function removeScriptOnLoad() { + globalThis.setTimeout(() => script?.remove(), 5000); } })(globalThis); diff --git a/apps/browser/src/autofill/fido2/content/messaging/message.ts b/apps/browser/src/autofill/fido2/content/messaging/message.ts index d42c10a5d88..5815be9eb60 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/message.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/message.ts @@ -18,7 +18,7 @@ export enum MessageType { } /** - * The params provided by the page-script are created in an insecure environemnt and + * The params provided by the page-script are created in an insecure environment and * should not be trusted. This type is used to ensure that the content-script does not * trust the `origin` or `sameOriginWithAncestors` params. */ @@ -38,7 +38,7 @@ export type CredentialCreationResponse = { }; /** - * The params provided by the page-script are created in an insecure environemnt and + * The params provided by the page-script are created in an insecure environment and * should not be trusted. This type is used to ensure that the content-script does not * trust the `origin` or `sameOriginWithAncestors` params. */ diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html new file mode 100644 index 00000000000..852fd4a0e81 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html @@ -0,0 +1,36 @@ +
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts new file mode 100644 index 00000000000..d9d492bdcc1 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts @@ -0,0 +1,39 @@ +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; + +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +@Component({ + selector: "app-fido2-cipher-row-v1", + templateUrl: "fido2-cipher-row-v1.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Fido2CipherRowV1Component { + @Output() onSelected = new EventEmitter(); + @Input() cipher: CipherView; + @Input() last: boolean; + @Input() title: string; + @Input() isSearching: boolean; + @Input() isSelected: boolean; + + protected selectCipher(c: CipherView) { + this.onSelected.emit(c); + } + + /** + * Returns a subname for the cipher. + * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId. + * @param c Cipher + * @returns + */ + protected getSubName(c: CipherView): string | null { + const fido2Credentials = c.login?.fido2Credentials; + + if (!fido2Credentials || fido2Credentials.length === 0) { + return null; + } + + const [fido2Credential] = fido2Credentials; + + return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null; + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html index 852fd4a0e81..0328a91bff5 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html @@ -1,36 +1,21 @@ -
-
- -
-
+ + + diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts index 25d623b1692..91bcd6494e6 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts @@ -1,19 +1,40 @@ +import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + ButtonModule, + IconButtonModule, + ItemModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "app-fido2-cipher-row", templateUrl: "fido2-cipher-row.component.html", changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + BadgeModule, + ButtonModule, + CommonModule, + IconButtonModule, + ItemModule, + JslibModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + ], }) export class Fido2CipherRowComponent { @Output() onSelected = new EventEmitter(); @Input() cipher: CipherView; @Input() last: boolean; @Input() title: string; - @Input() isSearching: boolean; - @Input() isSelected: boolean; protected selectCipher(c: CipherView) { this.onSelected.emit(c); diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html new file mode 100644 index 00000000000..9f6c0aca50d --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html @@ -0,0 +1,52 @@ + + + + +
+ +
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts new file mode 100644 index 00000000000..cf79dfc6520 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts @@ -0,0 +1,113 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { ConnectedPosition } from "@angular/cdk/overlay"; +import { Component } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { NeverDomains } from "@bitwarden/common/models/domain/domain-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"; + +import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; +import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; + +@Component({ + selector: "app-fido2-use-browser-link-v1", + templateUrl: "fido2-use-browser-link-v1.component.html", + animations: [ + trigger("transformPanel", [ + state( + "void", + style({ + opacity: 0, + }), + ), + transition( + "void => open", + animate( + "100ms linear", + style({ + opacity: 1, + }), + ), + ), + transition("* => void", animate("100ms linear", style({ opacity: 0 }))), + ]), + ], +}) +export class Fido2UseBrowserLinkV1Component { + showOverlay = false; + isOpen = false; + overlayPosition: ConnectedPosition[] = [ + { + originX: "start", + originY: "bottom", + overlayX: "start", + overlayY: "top", + offsetY: 5, + }, + ]; + + protected fido2PopoutSessionData$ = fido2PopoutSessionData$(); + + constructor( + private domainSettingsService: DomainSettingsService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + ) {} + + toggle() { + this.isOpen = !this.isOpen; + } + + close() { + this.isOpen = false; + } + + /** + * Aborts the current FIDO2 session and fallsback to the browser. + * @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts. + */ + protected async abort(excludeDomain = true) { + this.close(); + const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); + + if (!excludeDomain) { + this.abortSession(sessionData.sessionId); + return; + } + // Show overlay to prevent the user from interacting with the page. + this.showOverlay = true; + await this.handleDomainExclusion(sessionData.senderUrl); + // Give the user a chance to see the toast before closing the popout. + await Utils.delay(2000); + this.abortSession(sessionData.sessionId); + } + + /** + * Excludes the domain from future FIDO2 prompts. + * @param uri - The domain uri to exclude from future FIDO2 prompts. + */ + private async handleDomainExclusion(uri: string) { + const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); + + const validDomain = Utils.getHostname(uri); + const savedDomains: NeverDomains = { + ...existingDomains, + }; + savedDomains[validDomain] = null; + + await this.domainSettingsService.setNeverDomains(savedDomains); + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("domainAddedToExcludedDomains", validDomain), + ); + } + + private abortSession(sessionId: string) { + BrowserFido2UserInterfaceSession.abortPopout(sessionId, true); + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts index d9a7c7c9cbc..86f13d29c7a 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts @@ -1,8 +1,11 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition } from "@angular/cdk/overlay"; +import { A11yModule } from "@angular/cdk/a11y"; +import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f @Component({ selector: "app-fido2-use-browser-link", templateUrl: "fido2-use-browser-link.component.html", + standalone: true, + imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule], animations: [ trigger("transformPanel", [ state( @@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent { * @param uri - The domain uri to exclude from future FIDO2 prompts. */ private async handleDomainExclusion(uri: string) { - const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); + const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); const validDomain = Utils.getHostname(uri); const savedDomains: NeverDomains = { - ...exisitingDomains, + ...existingDomains, }; savedDomains[validDomain] = null; diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html new file mode 100644 index 00000000000..8a052fbc5b7 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html @@ -0,0 +1,142 @@ + +
+
+
+ + + + + + +
+ + +
+ +
+
+
+ + + +
+

+ {{ subtitleText | i18n }} +

+ + +
+
+ +
+
+ +
+ +
+
+ + +
+ +
+
+
+
+ +
+

{{ "passkeyAlreadyExists" | i18n }}

+
+
+ +
+
+ +
+
+ +
+

{{ "noPasskeysFoundForThisApplication" | i18n }}

+
+ +
+
+ + +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts new file mode 100644 index 00000000000..d6026a8c7a0 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts @@ -0,0 +1,443 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { + BehaviorSubject, + combineLatest, + concatMap, + filter, + firstValueFrom, + map, + Observable, + Subject, + take, + takeUntil, +} from "rxjs"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { DialogService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; +import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; +import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; +import { + BrowserFido2Message, + BrowserFido2UserInterfaceSession, + BrowserFido2MessageTypes, +} from "../../fido2/services/browser-fido2-user-interface.service"; + +interface ViewData { + message: BrowserFido2Message; + fallbackSupported: boolean; +} + +@Component({ + selector: "app-fido2-v1", + templateUrl: "fido2-v1.component.html", + styleUrls: [], +}) +export class Fido2V1Component implements OnInit, OnDestroy { + private destroy$ = new Subject(); + private hasSearched = false; + + protected cipher: CipherView; + protected searchTypeSearch = false; + protected searchPending = false; + protected searchText: string; + protected url: string; + protected hostname: string; + protected data$: Observable; + protected sessionId?: string; + protected senderTabId?: string; + protected ciphers?: CipherView[] = []; + protected displayedCiphers?: CipherView[] = []; + protected loading = false; + protected subtitleText: string; + protected credentialText: string; + protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; + + private message$ = new BehaviorSubject(null); + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private cipherService: CipherService, + private platformUtilsService: PlatformUtilsService, + private domainSettingsService: DomainSettingsService, + private searchService: SearchService, + private logService: LogService, + private dialogService: DialogService, + private browserMessagingApi: ZonedMessageListenerService, + private passwordRepromptService: PasswordRepromptService, + private fido2UserVerificationService: Fido2UserVerificationService, + private accountService: AccountService, + ) {} + + ngOnInit() { + this.searchTypeSearch = !this.platformUtilsService.isSafari(); + + const queryParams$ = this.activatedRoute.queryParamMap.pipe( + take(1), + map((queryParamMap) => ({ + sessionId: queryParamMap.get("sessionId"), + senderTabId: queryParamMap.get("senderTabId"), + senderUrl: queryParamMap.get("senderUrl"), + })), + ); + + combineLatest([ + queryParams$, + this.browserMessagingApi.messageListener$() as Observable, + ]) + .pipe( + concatMap(async ([queryParams, message]) => { + this.sessionId = queryParams.sessionId; + this.senderTabId = queryParams.senderTabId; + this.url = queryParams.senderUrl; + // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. + if ( + message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && + message.sessionId !== queryParams.sessionId + ) { + this.abort(false); + return; + } + + // Ignore messages that don't belong to the current session. + if (message.sessionId !== queryParams.sessionId) { + return; + } + + if (message.type === BrowserFido2MessageTypes.AbortRequest) { + this.abort(false); + return; + } + + return message; + }), + filter((message) => !!message), + takeUntil(this.destroy$), + ) + .subscribe((message) => { + this.message$.next(message); + }); + + this.data$ = this.message$.pipe( + filter((message) => message != undefined), + concatMap(async (message) => { + switch (message.type) { + case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + + this.ciphers = (await this.cipherService.getAllDecrypted()).filter( + (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, + ); + this.displayedCiphers = this.ciphers.filter( + (cipher) => + cipher.login.matchesUri(this.url, equivalentDomains) && + this.hasNoOtherPasskeys(cipher, message.userHandle), + ); + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case BrowserFido2MessageTypes.PickCredentialRequest: { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.ciphers = await Promise.all( + message.cipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + }), + ); + this.displayedCiphers = [...this.ciphers]; + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.ciphers = await Promise.all( + message.existingCipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + }), + ); + this.displayedCiphers = [...this.ciphers]; + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + } + + this.subtitleText = + this.displayedCiphers.length > 0 + ? this.getCredentialSubTitleText(message.type) + : "noMatchingPasskeyLogin"; + + this.credentialText = this.getCredentialButtonText(message.type); + return { + message, + fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, + }; + }), + takeUntil(this.destroy$), + ); + + queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { + this.send({ + sessionId: queryParams.sessionId, + type: BrowserFido2MessageTypes.ConnectResponse, + }); + }); + } + + protected async submit() { + const data = this.message$.value; + if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { + // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); + + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher.id, + type: BrowserFido2MessageTypes.PickCredentialResponse, + userVerified, + }); + } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + if (this.cipher.login.hasFido2Credentials) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "overwritePasskey" }, + content: { key: "overwritePasskeyAlert" }, + type: "info", + }); + + if (!confirmed) { + return false; + } + } + + // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); + + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher.id, + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, + userVerified, + }); + } + + this.loading = true; + } + + protected async saveNewLogin() { + const data = this.message$.value; + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + const name = data.credentialName || data.rpId; + // TODO: Revert to check for user verification once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + await this.createNewCipher(name, data.userName); + + // We are bypassing user verification pending approval. + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher?.id, + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, + userVerified: data.userVerification, + }); + } + + this.loading = true; + } + + getCredentialSubTitleText(messageType: string): string { + return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest + ? "chooseCipherForPasskeySave" + : "logInWithPasskeyQuestion"; + } + + getCredentialButtonText(messageType: string): string { + return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest + ? "savePasskey" + : "confirm"; + } + + selectedPasskey(item: CipherView) { + this.cipher = item; + } + + viewPasskey() { + // 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(["/view-cipher"], { + queryParams: { + cipherId: this.cipher.id, + uilocation: "popout", + senderTabId: this.senderTabId, + sessionId: this.sessionId, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); + } + + addCipher() { + const data = this.message$.value; + + if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + 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.router.navigate(["/add-cipher"], { + queryParams: { + name: data.credentialName || data.rpId, + uri: this.url, + type: CipherType.Login.toString(), + uilocation: "popout", + username: data.userName, + senderTabId: this.senderTabId, + sessionId: this.sessionId, + userVerification: data.userVerification, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); + } + + protected async search() { + this.hasSearched = await this.searchService.isSearchable(this.searchText); + this.searchPending = true; + if (this.hasSearched) { + this.displayedCiphers = await this.searchService.searchCiphers( + this.searchText, + null, + this.ciphers, + ); + } else { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + this.displayedCiphers = this.ciphers.filter((cipher) => + cipher.login.matchesUri(this.url, equivalentDomains), + ); + } + this.searchPending = false; + this.selectedPasskey(this.displayedCiphers[0]); + } + + abort(fallback: boolean) { + this.unload(fallback); + window.close(); + } + + unload(fallback = false) { + this.send({ + sessionId: this.sessionId, + type: BrowserFido2MessageTypes.AbortResponse, + fallbackRequested: fallback, + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private buildCipher(name: string, username: string) { + this.cipher = new CipherView(); + this.cipher.name = name; + + this.cipher.type = CipherType.Login; + this.cipher.login = new LoginView(); + this.cipher.login.username = username; + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.login.uris[0].uri = this.url; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + this.cipher.reprompt = CipherRepromptType.None; + } + + private async createNewCipher(name: string, username: string) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.buildCipher(name, username); + const cipher = await this.cipherService.encrypt(this.cipher, activeUserId); + try { + await this.cipherService.createWithServer(cipher); + this.cipher.id = cipher.id; + } catch (e) { + this.logService.error(e); + } + } + + // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. + private async handleUserVerification( + userVerificationRequested: boolean, + cipher: CipherView, + ): Promise { + const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0; + + if (masterPasswordRepromptRequired) { + return await this.passwordRepromptService.showPasswordPrompt(); + } + + return userVerificationRequested; + } + + private send(msg: BrowserFido2Message) { + BrowserFido2UserInterfaceSession.sendMessage({ + sessionId: this.sessionId, + ...msg, + }); + } + + /** + * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle + * @param userHandle + */ + private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { + return true; + } + + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.html b/apps/browser/src/autofill/popup/fido2/fido2.component.html index 9036d6d991c..00cd55d31b5 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.html @@ -1,136 +1,134 @@ - -
-
-
- - - + diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 8bd667c17fb..c389e9ad5b8 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -1,4 +1,6 @@ +import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { BehaviorSubject, @@ -13,13 +15,14 @@ import { takeUntil, } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -27,17 +30,39 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view" import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; -import { DialogService } from "@bitwarden/components"; +import { + ButtonModule, + DialogService, + Icons, + ItemModule, + NoItemsModule, + SearchModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; import { BrowserFido2Message, BrowserFido2UserInterfaceSession, + BrowserFido2MessageTypes, } from "../../fido2/services/browser-fido2-user-interface.service"; +import { Fido2CipherRowComponent } from "./fido2-cipher-row.component"; +import { Fido2UseBrowserLinkComponent } from "./fido2-use-browser-link.component"; + +const PasskeyActions = { + Register: "register", + Authenticate: "authenticate", +} as const; + +type PasskeyActionValue = (typeof PasskeyActions)[keyof typeof PasskeyActions]; + interface ViewData { message: BrowserFido2Message; fallbackSupported: boolean; @@ -46,28 +71,45 @@ interface ViewData { @Component({ selector: "app-fido2", templateUrl: "fido2.component.html", - styleUrls: [], + standalone: true, + imports: [ + ButtonModule, + CommonModule, + Fido2CipherRowComponent, + Fido2UseBrowserLinkComponent, + FormsModule, + ItemModule, + JslibModule, + NoItemsModule, + PopupHeaderComponent, + PopupPageComponent, + SearchModule, + SectionComponent, + SectionHeaderComponent, + ], }) export class Fido2Component implements OnInit, OnDestroy { private destroy$ = new Subject(); - private hasSearched = false; - - protected cipher: CipherView; - protected searchTypeSearch = false; - protected searchPending = false; - protected searchText: string; - protected url: string; - protected hostname: string; - protected data$: Observable; - protected sessionId?: string; - protected senderTabId?: string; - protected ciphers?: CipherView[] = []; - protected displayedCiphers?: CipherView[] = []; - protected loading = false; - protected subtitleText: string; - protected credentialText: string; - private message$ = new BehaviorSubject(null); + private hasSearched = false; + protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; + protected cipher: CipherView; + protected ciphers?: CipherView[] = []; + protected data$: Observable; + protected displayedCiphers?: CipherView[] = []; + protected equivalentDomains: Set; + protected equivalentDomainsURL: string; + protected hostname: string; + protected loading = false; + protected noResultsIcon = Icons.NoResults; + protected passkeyAction: PasskeyActionValue = PasskeyActions.Register; + protected PasskeyActions = PasskeyActions; + protected searchText: string; + protected searchTypeSearch = false; + protected senderTabId?: string; + protected sessionId?: string; + protected showNewPasskeyButton: boolean = false; + protected url: string; constructor( private router: Router, @@ -80,8 +122,8 @@ export class Fido2Component implements OnInit, OnDestroy { private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, private passwordRepromptService: PasswordRepromptService, - private fido2UserVerificationService: Fido2UserVerificationService, private accountService: AccountService, + private fido2UserVerificationService: Fido2UserVerificationService, ) {} ngOnInit() { @@ -107,7 +149,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.url = queryParams.senderUrl; // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. if ( - message.type === "NewSessionCreatedRequest" && + message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && message.sessionId !== queryParams.sessionId ) { this.abort(false); @@ -119,7 +161,7 @@ export class Fido2Component implements OnInit, OnDestroy { return; } - if (message.type === "AbortRequest") { + if (message.type === BrowserFido2MessageTypes.AbortRequest) { this.abort(false); return; } @@ -137,7 +179,7 @@ export class Fido2Component implements OnInit, OnDestroy { filter((message) => message != undefined), concatMap(async (message) => { switch (message.type) { - case "ConfirmNewCredentialRequest": { + case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { const equivalentDomains = await firstValueFrom( this.domainSettingsService.getUrlEquivalentDomains(this.url), ); @@ -145,19 +187,22 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = (await this.cipherService.getAllDecrypted()).filter( (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, ); + this.displayedCiphers = this.ciphers.filter( (cipher) => cipher.login.matchesUri(this.url, equivalentDomains) && - this.hasNoOtherPasskeys(cipher, message.userHandle), + this.cipherHasNoOtherPasskeys(cipher, message.userHandle), ); - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + this.passkeyAction = PasskeyActions.Register; + + // @TODO fix new cipher creation for other fido2 registration message types and remove `showNewPasskeyButton` from the template + this.showNewPasskeyButton = true; + break; } - case "PickCredentialRequest": { + case BrowserFido2MessageTypes.PickCredentialRequest: { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -170,14 +215,15 @@ export class Fido2Component implements OnInit, OnDestroy { ); }), ); + this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + + this.passkeyAction = PasskeyActions.Authenticate; + break; } - case "InformExcludedCredentialRequest": { + case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -190,40 +236,42 @@ export class Fido2Component implements OnInit, OnDestroy { ); }), ); + this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + this.passkeyAction = PasskeyActions.Register; + + break; + } + + case BrowserFido2MessageTypes.InformCredentialNotFoundRequest: { + this.passkeyAction = PasskeyActions.Authenticate; + break; } } - this.subtitleText = - this.displayedCiphers.length > 0 - ? this.getCredentialSubTitleText(message.type) - : "noMatchingPasskeyLogin"; - - this.credentialText = this.getCredentialButtonText(message.type); return { message, fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, }; }), + takeUntil(this.destroy$), ); queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { this.send({ sessionId: queryParams.sessionId, - type: "ConnectResponse", + type: BrowserFido2MessageTypes.ConnectResponse, }); }); } protected async submit() { const data = this.message$.value; - if (data?.type === "PickCredentialRequest") { + + if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); @@ -231,10 +279,10 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher.id, - type: "PickCredentialResponse", + type: BrowserFido2MessageTypes.PickCredentialResponse, userVerified, }); - } else if (data?.type === "ConfirmNewCredentialRequest") { + } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { if (this.cipher.login.hasFido2Credentials) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "overwritePasskey" }, @@ -254,7 +302,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher.id, - type: "ConfirmNewCredentialResponse", + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, userVerified, }); } @@ -264,7 +312,8 @@ export class Fido2Component implements OnInit, OnDestroy { protected async saveNewLogin() { const data = this.message$.value; - if (data?.type === "ConfirmNewCredentialRequest") { + + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { const name = data.credentialName || data.rpId; // TODO: Revert to check for user verification once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 @@ -274,7 +323,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher?.id, - type: "ConfirmNewCredentialResponse", + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, userVerified: data.userVerification, }); } @@ -282,59 +331,47 @@ export class Fido2Component implements OnInit, OnDestroy { this.loading = true; } - getCredentialSubTitleText(messageType: string): string { - return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey"; - } - - getCredentialButtonText(messageType: string): string { - return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm"; - } - - selectedPasskey(item: CipherView) { + async handleCipherItemSelect(item: CipherView) { this.cipher = item; + + await this.submit(); } - viewPasskey() { - // 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(["/view-cipher"], { - queryParams: { - cipherId: this.cipher.id, - uilocation: "popout", - senderTabId: this.senderTabId, - sessionId: this.sessionId, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - addCipher() { + async addCipher() { const data = this.message$.value; - if (data?.type !== "ConfirmNewCredentialRequest") { - return; + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + await this.router.navigate(["/add-cipher"], { + queryParams: { + type: CipherType.Login.toString(), + name: data.credentialName || data.rpId, + uri: this.url, + uilocation: "popout", + username: data.userName, + senderTabId: this.senderTabId, + sessionId: this.sessionId, + userVerification: data.userVerification, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); } - // 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(["/add-cipher"], { - queryParams: { - name: data.credentialName || data.rpId, - uri: this.url, - type: CipherType.Login.toString(), - uilocation: "popout", - username: data.userName, - senderTabId: this.senderTabId, - sessionId: this.sessionId, - userVerification: data.userVerification, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); + return; + } + + async getEquivalentDomains() { + if (this.equivalentDomainsURL !== this.url) { + this.equivalentDomainsURL = this.url; + this.equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + } + + return this.equivalentDomains; } protected async search() { this.hasSearched = await this.searchService.isSearchable(this.searchText); - this.searchPending = true; if (this.hasSearched) { this.displayedCiphers = await this.searchService.searchCiphers( this.searchText, @@ -342,15 +379,11 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers, ); } else { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); + const equivalentDomains = await this.getEquivalentDomains(); this.displayedCiphers = this.ciphers.filter((cipher) => cipher.login.matchesUri(this.url, equivalentDomains), ); } - this.searchPending = false; - this.selectedPasskey(this.displayedCiphers[0]); } abort(fallback: boolean) { @@ -361,7 +394,7 @@ export class Fido2Component implements OnInit, OnDestroy { unload(fallback = false) { this.send({ sessionId: this.sessionId, - type: "AbortResponse", + type: BrowserFido2MessageTypes.AbortResponse, fallbackRequested: fallback, }); } @@ -427,13 +460,11 @@ export class Fido2Component implements OnInit, OnDestroy { * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle * @param userHandle */ - private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + private cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { return true; } - return cipher.login.fido2Credentials.some((passkey) => { - passkey.userHandle === userHandle; - }); + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); } } diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index f27e2faf3fa..089c48cd230 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -122,29 +122,9 @@ export class InlineMenuFieldQualificationService ...this.identityAddressAutoCompleteValues, ...this.identityCountryAutocompleteValues, ...this.identityPhoneNumberAutocompleteValues, + this.identityCompanyAutocompleteValue, this.identityPostalCodeAutocompleteValue, ]); - private identityFieldKeywords = [ - ...new Set([ - ...IdentityAutoFillConstants.TitleFieldNames, - ...IdentityAutoFillConstants.FullNameFieldNames, - ...IdentityAutoFillConstants.FirstnameFieldNames, - ...IdentityAutoFillConstants.MiddlenameFieldNames, - ...IdentityAutoFillConstants.LastnameFieldNames, - ...IdentityAutoFillConstants.AddressFieldNames, - ...IdentityAutoFillConstants.Address1FieldNames, - ...IdentityAutoFillConstants.Address2FieldNames, - ...IdentityAutoFillConstants.Address3FieldNames, - ...IdentityAutoFillConstants.PostalCodeFieldNames, - ...IdentityAutoFillConstants.CityFieldNames, - ...IdentityAutoFillConstants.StateFieldNames, - ...IdentityAutoFillConstants.CountryFieldNames, - ...IdentityAutoFillConstants.CompanyFieldNames, - ...IdentityAutoFillConstants.PhoneFieldNames, - ...IdentityAutoFillConstants.EmailFieldNames, - ...IdentityAutoFillConstants.UserNameFieldNames, - ]), - ]; private inlineMenuFieldQualificationFlagSet = false; constructor() { @@ -288,14 +268,7 @@ export class InlineMenuFieldQualificationService return false; } - if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) { - return true; - } - - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, this.identityFieldKeywords, false) - ); + return this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues); } /** diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 0da55cbda5f..124cf6a78d8 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1267,6 +1267,18 @@ export default class MainBackground { ); } + // If the user is logged out, switch to the next account + const active = await firstValueFrom(this.accountService.activeAccount$); + if (active != null) { + const authStatus = await firstValueFrom( + this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])), + ); + if (authStatus === AuthenticationStatus.LoggedOut) { + const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$); + await this.switchAccount(nextUpAccount?.id); + } + } + await this.initOverlayAndTabsBackground(); return new Promise((resolve) => { diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 82e673a9e54..aa8955035dd 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -41,6 +41,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component" import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -127,12 +128,11 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { state: "home" }, }, - { + ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { path: "fido2", - component: Fido2Component, canActivate: [fido2AuthGuard], data: { state: "fido2" }, - }, + }), { path: "login", component: LoginComponent, @@ -304,7 +304,6 @@ const routes: Routes = [ }, ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { path: "notifications", - component: NotificationsSettingsV1Component, canActivate: [authGuard], data: { state: "notifications" }, }), @@ -338,7 +337,6 @@ const routes: Routes = [ }, ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { path: "excluded-domains", - component: ExcludedDomainsV1Component, canActivate: [authGuard], data: { state: "excluded-domains" }, }), diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 56ddd3c6ba3..f8d3c691051 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -35,8 +35,11 @@ import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component"; import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; +import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component"; import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -112,6 +115,9 @@ import "../platform/popup/locales"; ServicesModule, DialogModule, ExcludedDomainsComponent, + Fido2CipherRowComponent, + Fido2Component, + Fido2UseBrowserLinkComponent, FilePopoutCalloutComponent, AvatarModule, AccountComponent, @@ -140,8 +146,8 @@ import "../platform/popup/locales"; CurrentTabComponent, EnvironmentComponent, ExcludedDomainsV1Component, - Fido2CipherRowComponent, - Fido2UseBrowserLinkComponent, + Fido2CipherRowV1Component, + Fido2UseBrowserLinkV1Component, FolderAddEditComponent, FoldersComponent, VaultFilterComponent, @@ -180,7 +186,7 @@ import "../platform/popup/locales"; ViewCustomFieldsComponent, RemovePasswordComponent, VaultSelectComponent, - Fido2Component, + Fido2V1Component, AutofillV1Component, EnvironmentSelectorComponent, ], diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index d54655f15e2..2fd903fedc7 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -217,7 +217,7 @@ app-vault-attachments { } } -app-fido2 { +app-fido2-v1 { .auth-wrapper { display: flex; flex-direction: column; diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index 1ec0f52aa6d..c5fea3c2b8c 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -15,6 +15,7 @@ import { SectionComponent, SectionHeaderComponent, ToastService, + TypographyModule, } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -30,6 +31,7 @@ import { PasswordRepromptService } from "@bitwarden/vault"; SectionHeaderComponent, MenuModule, IconButtonModule, + TypographyModule, ], }) export class TrashListItemsContainerComponent { diff --git a/apps/browser/src/vault/popup/settings/trash.component.html b/apps/browser/src/vault/popup/settings/trash.component.html index ab3b6716504..146e4161671 100644 --- a/apps/browser/src/vault/popup/settings/trash.component.html +++ b/apps/browser/src/vault/popup/settings/trash.component.html @@ -5,7 +5,11 @@ - + {{ "trashWarning" | i18n }} diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index b44d994f4e7..a6119a2e0a7 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -12,7 +12,7 @@ "baseUrl": ".", "lib": ["ES2021.String"], "paths": { - "@bitwarden/admin-console": ["../../libs/admin-console/src"], + "@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], diff --git a/apps/cli/config/development.json b/apps/cli/config/development.json index bc06f69d657..f57c3d9bc38 100644 --- a/apps/cli/config/development.json +++ b/apps/cli/config/development.json @@ -1,5 +1,5 @@ { "flags": { - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/cli/config/production.json b/apps/cli/config/production.json index bc06f69d657..f57c3d9bc38 100644 --- a/apps/cli/config/production.json +++ b/apps/cli/config/production.json @@ -1,5 +1,5 @@ { "flags": { - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/cli/src/admin-console/commands/confirm.command.ts b/apps/cli/src/admin-console/commands/confirm.command.ts index c6d9e4bd574..066cca48f13 100644 --- a/apps/cli/src/admin-console/commands/confirm.command.ts +++ b/apps/cli/src/admin-console/commands/confirm.command.ts @@ -1,6 +1,8 @@ +import { + OrganizationUserApiService, + OrganizationUserConfirmRequest, +} from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -10,7 +12,7 @@ export class ConfirmCommand { constructor( private apiService: ApiService, private cryptoService: CryptoService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, ) {} async run(object: string, id: string, cmdOptions: Record): Promise { @@ -42,7 +44,7 @@ export class ConfirmCommand { if (orgKey == null) { throw new Error("No encryption key for this organization."); } - const orgUser = await this.organizationUserService.getOrganizationUser( + const orgUser = await this.organizationUserApiService.getOrganizationUser( options.organizationId, id, ); @@ -54,7 +56,7 @@ export class ConfirmCommand { const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey); const req = new OrganizationUserConfirmRequest(); req.key = key.encryptedString; - await this.organizationUserService.postOrganizationUserConfirm( + await this.organizationUserApiService.postOrganizationUserConfirm( options.organizationId, id, req, diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 88574635e1c..692a5c9bab3 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -1,10 +1,10 @@ import { firstValueFrom } from "rxjs"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { EventType } from "@bitwarden/common/enums"; import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -35,7 +35,7 @@ export class ListCommand { private collectionService: CollectionService, private organizationService: OrganizationService, private searchService: SearchService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, private apiService: ApiService, private eventCollectionService: EventCollectionService, ) {} @@ -211,7 +211,7 @@ export class ListCommand { } try { - const response = await this.organizationUserService.getAllUsers(options.organizationId); + const response = await this.organizationUserApiService.getAllUsers(options.organizationId); const res = new ListResponse( response.data.map((r) => { const u = new OrganizationUserResponse(); diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 6e0fa1c43c3..d7ef9ac871d 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -71,7 +71,7 @@ export class OssServeConfigurator { this.serviceContainer.collectionService, this.serviceContainer.organizationService, this.serviceContainer.searchService, - this.serviceContainer.organizationUserService, + this.serviceContainer.organizationUserApiService, this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, ); @@ -114,7 +114,7 @@ export class OssServeConfigurator { this.confirmCommand = new ConfirmCommand( this.serviceContainer.apiService, this.serviceContainer.cryptoService, - this.serviceContainer.organizationUserService, + this.serviceContainer.organizationUserApiService, ); this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService); this.shareCommand = new ShareCommand( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index c3fd55fa8b2..fb77e41a4b6 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -4,6 +4,10 @@ import * as path from "path"; import * as jsdom from "jsdom"; import { firstValueFrom } from "rxjs"; +import { + OrganizationUserApiService, + DefaultOrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { InternalUserDecryptionOptionsServiceAbstraction, AuthRequestService, @@ -16,12 +20,10 @@ import { import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; -import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; @@ -184,7 +186,7 @@ export class ServiceContainer { environmentService: EnvironmentService; cipherService: CipherService; folderService: InternalFolderService; - organizationUserService: OrganizationUserService; + organizationUserApiService: OrganizationUserApiService; collectionService: CollectionService; vaultTimeoutService: VaultTimeoutService; masterPasswordService: InternalMasterPasswordServiceAbstraction; @@ -492,7 +494,7 @@ export class ServiceContainer { this.providerService = new ProviderService(this.stateProvider); - this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService); + this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 9cf30086166..2dad9a7c68a 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -108,7 +108,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.collectionService, this.serviceContainer.organizationService, this.serviceContainer.searchService, - this.serviceContainer.organizationUserService, + this.serviceContainer.organizationUserApiService, this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, ); @@ -412,7 +412,7 @@ export class VaultProgram extends BaseProgram { const command = new ConfirmCommand( this.serviceContainer.apiService, this.serviceContainer.cryptoService, - this.serviceContainer.organizationUserService, + this.serviceContainer.organizationUserApiService, ); const response = await command.run(object, id, cmd); this.processResponse(response); diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 0a34b05496f..d84dcdaf675 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -13,6 +13,7 @@ "baseUrl": ".", "paths": { "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"], "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/common/*": ["../../libs/common/src/*"], diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json index 7f18c63878b..7a8659feffe 100644 --- a/apps/desktop/config/base.json +++ b/apps/desktop/config/base.json @@ -1,6 +1,6 @@ { "devFlags": {}, "flags": { - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json index 7f18c63878b..7a8659feffe 100644 --- a/apps/desktop/config/development.json +++ b/apps/desktop/config/development.json @@ -1,6 +1,6 @@ { "devFlags": {}, "flags": { - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json index bc06f69d657..f57c3d9bc38 100644 --- a/apps/desktop/config/production.json +++ b/apps/desktop/config/production.json @@ -1,5 +1,5 @@ { "flags": { - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/desktop_native/.gitignore b/apps/desktop/desktop_native/.gitignore index 96e7a71e1b0..1cfa7dafc20 100644 --- a/apps/desktop/desktop_native/.gitignore +++ b/apps/desktop/desktop_native/.gitignore @@ -4,3 +4,4 @@ index.node **/.DS_Store npm-debug.log* *.node +dist diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 3c0c5c41822..8b498f2c65e 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.126" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c4eae4b7fc8dcb0032eb3b1beee46b38d371cdeaf2d0c64b9944f6f69ad7755" +checksum = "54ccead7d199d584d139148b04b4a368d1ec7556a1d9ea2548febb1b9d49f9a4" dependencies = [ "cc", "cxxbridge-flags", @@ -452,9 +452,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.126" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c822bf7fb755d97328d6c337120b6f843678178751cba33c9da25cf522272e0" +checksum = "c77953e99f01508f89f55c494bfa867171ef3a6c8cea03d26975368f2121a5c1" dependencies = [ "cc", "codespan-reporting", @@ -467,21 +467,30 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.126" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719d6197dc016c88744aff3c0d0340a01ecce12e8939fc282e7c8f583ee64bc6" +checksum = "65777e06cc48f0cb0152024c77d6cf9e4bdb4408e7b48bea993d42fa0f5b02b6" [[package]] name = "cxxbridge-macro" -version = "1.0.126" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35de3b547387863c8f82013c4f79f1c2162edee956383e4089e1d04c18c4f16c" +checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-new" version = "0.6.0" @@ -503,10 +512,14 @@ dependencies = [ "base64", "cbc", "core-foundation", + "dirs", + "futures", "gio", + "interprocess", "keytar", "libc", "libsecret", + "log", "rand", "retry", "scopeguard", @@ -515,6 +528,7 @@ dependencies = [ "sha2", "thiserror", "tokio", + "tokio-util", "typenum", "widestring", "windows", @@ -531,6 +545,21 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "tokio", + "tokio-util", +] + +[[package]] +name = "desktop_proxy" +version = "0.0.0" +dependencies = [ + "anyhow", + "desktop_core", + "futures", + "log", + "simplelog", + "tokio", + "tokio-util", ] [[package]] @@ -543,6 +572,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dlib" version = "0.5.2" @@ -552,6 +602,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -646,6 +702,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -653,6 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -720,6 +792,7 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -896,9 +969,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -914,6 +987,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interprocess" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "keytar" version = "0.1.6" @@ -951,6 +1045,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "libsecret" version = "0.5.0" @@ -1039,10 +1143,21 @@ dependencies = [ ] [[package]] -name = "napi" -version = "2.16.6" +name = "mio" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc300228808a0e6aea5a58115c82889240bcf8dab16fc25ad675b33e454b368" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "napi" +version = "2.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633e41b2b983cf7983134f0c50986ca524d0caf38a2c6fc893ea3fa2e26abb0c" dependencies = [ "bitflags", "ctor", @@ -1060,9 +1175,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.5" +version = "2.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4" +checksum = "70a8a778fd367b13c64232e58632514b795514ece491ce136d96e976d34a3eb8" dependencies = [ "cfg-if", "convert_case", @@ -1131,6 +1246,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -1141,6 +1262,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -1242,9 +1372,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -1255,6 +1385,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-stream" version = "0.2.0" @@ -1358,6 +1494,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1433,6 +1575,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.3" @@ -1442,6 +1590,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.6" @@ -1623,6 +1782,17 @@ dependencies = [ "libc", ] +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1638,6 +1808,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1646,9 +1826,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1716,6 +1896,39 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.38.0" @@ -1724,9 +1937,13 @@ checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", + "libc", + "mio", "num_cpus", "pin-project-lite", + "socket2", "tokio-macros", + "windows-sys 0.48.0", ] [[package]] @@ -1740,6 +1957,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.19" @@ -2035,6 +2265,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index c6b77473b2a..6525b38162d 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["napi", "core"] +members = ["napi", "core", "proxy"] diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js new file mode 100644 index 00000000000..f2f012bf088 --- /dev/null +++ b/apps/desktop/desktop_native/build.js @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const child_process = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const process = require("process"); + +let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform"; + +function buildNapiModule(target, release = true) { + const targetArg = target ? `--target ${target}` : ""; + const releaseArg = release ? "--release" : ""; + return child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") }); +} + +function buildProxyBin(target, release = true) { + const targetArg = target ? `--target ${target}` : ""; + const releaseArg = release ? "--release" : ""; + return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); +} + +if (!crossPlatform) { + console.log("Building native modules in debug mode for the native architecture"); + buildNapiModule(false, false); + buildProxyBin(false, false); + return; +} + +// Note that targets contains pairs of [rust target, node arch] +// We do this to move the output binaries to a location that can +// be easily accessed from electron-builder using ${os} and ${arch} +let targets = []; +switch (process.platform) { + case "win32": + targets = [ + ["i686-pc-windows-msvc", 'ia32'], + ["x86_64-pc-windows-msvc", 'x64'], + ["aarch64-pc-windows-msvc", 'arm64'] + ]; + break; + + case "darwin": + targets = [ + ["x86_64-apple-darwin", 'x64'], + ["aarch64-apple-darwin", 'arm64'] + ]; + break; + + default: + targets = [ + ['x86_64-unknown-linux-musl', 'x64'] + ]; + + process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; + process.env["PKG_CONFIG_ALL_STATIC"] = "1"; + break; +} + +console.log("Cross building native modules for the targets: ", targets.map(([target, _]) => target).join(", ")); + +fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true }); + +targets.forEach(([target, nodeArch]) => { + buildNapiModule(target); + buildProxyBin(target); + + const ext = process.platform === "win32" ? ".exe" : ""; + fs.copyFileSync(path.join(__dirname, "target", target, "release", `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`)); +}); diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index d8859208837..46b22a5b74e 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -6,9 +6,21 @@ version = "0.0.0" publish = false [features] -default = [] +default = ["sys"] manual_test = [] +sys = [ + "dep:widestring", + "dep:windows", + "dep:core-foundation", + "dep:security-framework", + "dep:security-framework-sys", + "dep:gio", + "dep:libsecret", + "dep:zbus", + "dep:zbus_polkit", +] + [dependencies] aes = "=0.8.4" anyhow = "=1.0.86" @@ -17,17 +29,22 @@ arboard = { version = "=3.4.0", default-features = false, features = [ ] } base64 = "=0.22.1" cbc = { version = "=0.1.2", features = ["alloc"] } +dirs = "=5.0.1" +futures = "=0.3.30" +interprocess = { version = "=2.2.1", features = ["tokio"] } libc = "=0.2.155" +log = "=0.4.22" rand = "=0.8.5" retry = "=2.0.0" scopeguard = "=1.2.0" sha2 = "=0.10.8" thiserror = "=1.0.61" tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] } +tokio-util = "=0.7.11" typenum = "=1.17.0" [target.'cfg(windows)'.dependencies] -widestring = "=1.1.0" +widestring = { version = "=1.1.0", optional = true } windows = { version = "=0.57.0", features = [ "Foundation", "Security_Credentials_UI", @@ -38,18 +55,18 @@ windows = { version = "=0.57.0", features = [ "Win32_System_WinRT", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", -] } +], optional = true } [target.'cfg(windows)'.dev-dependencies] keytar = "=0.1.6" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "=0.9.4" -security-framework = "=2.11.0" -security-framework-sys = "=2.11.0" +core-foundation = { version = "=0.9.4", optional = true } +security-framework = { version = "=2.11.0", optional = true } +security-framework-sys = { version = "=2.11.0", optional = true } [target.'cfg(target_os = "linux")'.dependencies] -gio = "=0.19.5" -libsecret = "=0.5.0" -zbus = "=4.3.1" -zbus_polkit = "=4.0.0" +gio = { version = "=0.19.5", optional = true } +libsecret = { version = "=0.5.0", optional = true } +zbus = { version = "=4.3.1", optional = true } +zbus_polkit = { version = "=4.0.0", optional = true } diff --git a/apps/desktop/desktop_native/core/src/ipc/client.rs b/apps/desktop/desktop_native/core/src/ipc/client.rs new file mode 100644 index 00000000000..6e24a74629c --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/client.rs @@ -0,0 +1,102 @@ +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; + +use interprocess::local_socket::{ + tokio::{prelude::*, Stream}, + GenericFilePath, ToFsName, +}; +use log::{error, info}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + time::sleep, +}; + +use crate::ipc::NATIVE_MESSAGING_BUFFER_SIZE; + +pub async fn connect( + path: PathBuf, + send: tokio::sync::mpsc::Sender, + mut recv: tokio::sync::mpsc::Receiver, +) { + // Keep track of connection failures to make sure we don't leave the process as a zombie + let mut connection_failures = 0; + + loop { + match connect_inner(&path, &send, &mut recv).await { + Ok(()) => return, + Err(e) => { + connection_failures += 1; + if connection_failures >= 20 { + error!("Failed to connect to IPC server after 20 attempts: {e}"); + return; + } + + error!("Failed to connect to IPC server: {e}"); + } + } + + sleep(Duration::from_secs(5)).await; + } +} + +async fn connect_inner( + path: &Path, + send: &tokio::sync::mpsc::Sender, + recv: &mut tokio::sync::mpsc::Receiver, +) -> Result<(), Box> { + info!("Attempting to connect to {}", path.display()); + + let name = path.as_os_str().to_fs_name::()?; + let mut conn = Stream::connect(name).await?; + + info!("Connected to {}", path.display()); + + // This `connected` and the latter `disconnected` messages are the only ones that + // are sent from the Rust IPC code and not just forwarded from the desktop app. + // As it's only two, we hardcode the JSON values to avoid pulling in a JSON library. + send.send("{\"command\":\"connected\"}".to_owned()).await?; + + let mut buffer = vec![0; NATIVE_MESSAGING_BUFFER_SIZE]; + + // Listen to IPC messages + loop { + tokio::select! { + // Forward messages to the IPC server + msg = recv.recv() => { + match msg { + Some(msg) => { + conn.write_all(msg.as_bytes()).await?; + } + None => { + info!("Client channel closed"); + break; + }, + } + }, + + // Forward messages from the IPC server + res = conn.read(&mut buffer[..]) => { + match res { + Err(e) => { + error!("Error reading from IPC server: {e}"); + break; + } + Ok(0) => { + info!("Connection closed"); + break; + } + Ok(n) => { + let message = String::from_utf8_lossy(&buffer[..n]).to_string(); + send.send(message).await?; + } + } + } + } + } + + let _ = send.send("{\"command\":\"disconnected\"}".to_owned()).await; + + Ok(()) +} diff --git a/apps/desktop/desktop_native/core/src/ipc/mod.rs b/apps/desktop/desktop_native/core/src/ipc/mod.rs new file mode 100644 index 00000000000..6117dab38cf --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/mod.rs @@ -0,0 +1,64 @@ +pub mod client; +pub mod server; + +/// The maximum size of a message that can be sent over IPC. +/// According to the documentation, the maximum size sent to the browser is 1MB. +/// While the maximum size sent from the browser to the native messaging host is 4GB. +/// +/// Currently we are setting the maximum both ways to be 1MB. +/// +/// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side +/// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol +pub const NATIVE_MESSAGING_BUFFER_SIZE: usize = 1024 * 1024; + +/// The maximum number of messages that can be buffered in a channel. +/// This number is more or less arbitrary and can be adjusted as needed, +/// but ideally the messages should be processed as quickly as possible. +pub const MESSAGE_CHANNEL_BUFFER: usize = 32; + +/// Resolve the path to the IPC socket. +pub fn path(name: &str) -> std::path::PathBuf { + #[cfg(target_os = "windows")] + { + // Use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user. + // Hashing prevents problems with reserved characters and file length limitations. + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + use sha2::Digest; + let home = dirs::home_dir().unwrap(); + let hash = sha2::Sha256::digest(home.as_os_str().as_encoded_bytes()); + let hash_b64 = URL_SAFE_NO_PAD.encode(hash.as_slice()); + + format!(r"\\.\pipe\{hash_b64}.app.{name}").into() + } + + #[cfg(target_os = "macos")] + { + let mut home = dirs::home_dir().unwrap(); + + // When running in an unsandboxed environment, path is: /Users// + // While running sandboxed, it's different: /Users//Library/Containers/com.bitwarden.desktop/Data + // + // We want to use App Groups in /Users//Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop, + // so we need to remove all the components after the user. + // Note that we subtract 3 because the root directory is counted as a component (/, Users, ). + let num_components = home.components().count(); + for _ in 0..num_components - 3 { + home.pop(); + } + + home.join(format!( + "Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp/app.{name}" + )) + } + + #[cfg(target_os = "linux")] + { + // On Linux, we use the user's cache directory. + let home = dirs::cache_dir().unwrap(); + let path_dir = home.join("com.bitwarden.desktop"); + + // The chache directory might not exist, so create it + let _ = std::fs::create_dir_all(&path_dir); + path_dir.join(format!("app.{name}")) + } +} diff --git a/apps/desktop/desktop_native/core/src/ipc/server.rs b/apps/desktop/desktop_native/core/src/ipc/server.rs new file mode 100644 index 00000000000..053b4322203 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/ipc/server.rs @@ -0,0 +1,232 @@ +use std::{error::Error, path::Path, vec}; + +use futures::TryFutureExt; + +use anyhow::Result; +use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions}; +use log::{error, info}; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + sync::{broadcast, mpsc}, +}; +use tokio_util::sync::CancellationToken; + +use super::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE}; + +#[derive(Debug)] +pub struct Message { + pub client_id: u32, + pub kind: MessageType, + // This value should be Some for MessageType::Message and None for the rest + pub message: Option, +} + +#[derive(Debug)] +pub enum MessageType { + Connected, + Disconnected, + Message, +} + +pub struct Server { + cancel_token: CancellationToken, + server_to_clients_send: broadcast::Sender, +} + +impl Server { + /// Create and start the IPC server without blocking. + /// + /// # Parameters + /// + /// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + /// - `client_to_server_send`: This [`mpsc::Sender`] will receive all the [`Message`]'s that the clients send to this server. + pub fn start( + path: &Path, + client_to_server_send: mpsc::Sender, + ) -> Result> { + // If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first. + // Any processes that were using the old socket should remain connected to it but any new connections will use the new socket. + if !cfg!(windows) { + let _ = std::fs::remove_file(path); + } + + let name = path.as_os_str().to_fs_name::()?; + let opts = ListenerOptions::new().name(name); + let listener = opts.create_tokio()?; + + // This broadcast channel is used for sending messages to all connected clients, and so the sender + // will be stored in the server while the receiver will be cloned and passed to each client handler. + let (server_to_clients_send, server_to_clients_recv) = + broadcast::channel::(MESSAGE_CHANNEL_BUFFER); + + // This cancellation token allows us to cleanly stop the server and all the spawned + // tasks without having to wait on all the pending tasks finalizing first + let cancel_token = CancellationToken::new(); + + // Create the server and start listening for incoming connections + // in a separate task to avoid blocking the current task + let server = Server { + cancel_token: cancel_token.clone(), + server_to_clients_send, + }; + tokio::spawn(listen_incoming( + listener, + client_to_server_send, + server_to_clients_recv, + cancel_token, + )); + + Ok(server) + } + + /// Send a message over the IPC server to all the connected clients + /// + /// # Returns + /// + /// The number of clients that the message was sent to. Note that the number of messages + /// sent may be less than the number of connected clients if some clients disconnect while + /// the message is being sent. + pub fn send(&self, message: String) -> Result { + let sent = self.server_to_clients_send.send(message)?; + Ok(sent) + } + + /// Stop the IPC server. + pub fn stop(&self) { + self.cancel_token.cancel(); + } +} + +impl Drop for Server { + fn drop(&mut self) { + self.stop(); + } +} + +async fn listen_incoming( + listener: LocalSocketListener, + client_to_server_send: mpsc::Sender, + server_to_clients_recv: broadcast::Receiver, + cancel_token: CancellationToken, +) { + // We use a simple incrementing ID for each client + let mut next_client_id = 1_u32; + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("IPC server cancelled."); + break; + }, + + // A new client connection has been established + msg = listener.accept() => { + match msg { + Ok(client_stream) => { + let client_id = next_client_id; + next_client_id += 1; + + let future = handle_connection( + client_stream, + client_to_server_send.clone(), + // We resubscribe to the receiver here so this task can have it's own copy + // Note that this copy will only receive messages sent after this point, + // but that is okay, realistically we don't want any messages before we get a chance + // to send the connected message to the client, which is done inside [`handle_connection`] + server_to_clients_recv.resubscribe(), + cancel_token.clone(), + client_id + ); + tokio::spawn(future.map_err(|e| { + error!("Error handling connection: {}", e) + })); + }, + Err(e) => { + error!("Error accepting connection: {}", e); + break; + }, + } + } + } + } +} + +async fn handle_connection( + mut client_stream: impl AsyncRead + AsyncWrite + Unpin, + client_to_server_send: mpsc::Sender, + mut server_to_clients_recv: broadcast::Receiver, + cancel_token: CancellationToken, + client_id: u32, +) -> Result<(), Box> { + client_to_server_send + .send(Message { + client_id, + kind: MessageType::Connected, + message: None, + }) + .await?; + + let mut buf = vec![0u8; NATIVE_MESSAGING_BUFFER_SIZE]; + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + info!("Client {client_id} cancelled."); + break; + }, + + // Forward messages to the IPC clients + msg = server_to_clients_recv.recv() => { + match msg { + Ok(msg) => { + client_stream.write_all(msg.as_bytes()).await?; + }, + Err(e) => { + info!("Error reading message: {}", e); + break; + } + } + }, + + // Forwards messages from the IPC clients to the server + // Note that we also send connect and disconnect events so that + // the server can keep track of multiple clients + result = client_stream.read(&mut buf) => { + match result { + Err(e) => { + info!("Error reading from client {client_id}: {e}"); + + client_to_server_send.send(Message { + client_id, + kind: MessageType::Disconnected, + message: None, + }).await?; + break; + }, + Ok(0) => { + info!("Client {client_id} disconnected."); + + client_to_server_send.send(Message { + client_id, + kind: MessageType::Disconnected, + message: None, + }).await?; + break; + }, + Ok(size) => { + let msg = std::str::from_utf8(&buf[..size])?; + + client_to_server_send.send(Message { + client_id, + kind: MessageType::Message, + message: Some(msg.to_string()), + }).await?; + }, + + } + } + } + } + + Ok(()) +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index d23a285b4ac..3132c56f7f8 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "sys")] pub mod biometric; +#[cfg(feature = "sys")] pub mod clipboard; pub mod crypto; pub mod error; +pub mod ipc; +#[cfg(feature = "sys")] pub mod password; +#[cfg(feature = "sys")] pub mod process_isolation; +#[cfg(feature = "sys")] pub mod powermonitor; diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 942ccdba212..6fb710b0671 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -16,8 +16,10 @@ manual_test = [] [dependencies] anyhow = "=1.0.86" desktop_core = { path = "../core" } -napi = { version = "=2.16.6", features = ["async"] } -napi-derive = "=2.16.5" +napi = { version = "=2.16.7", features = ["async"] } +napi-derive = "=2.16.6" +tokio = { version = "1.38.0" } +tokio-util = "0.7.11" [build-dependencies] napi-build = "=2.1.3" diff --git a/apps/desktop/desktop_native/napi/build.js b/apps/desktop/desktop_native/napi/build.js deleted file mode 100644 index 6c92dbad1b6..00000000000 --- a/apps/desktop/desktop_native/napi/build.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const child_process = require("child_process"); -const process = require("process"); - -let targets = []; -switch (process.platform) { - case "win32": - targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]; - break; - - case "darwin": - targets = ["x86_64-apple-darwin", "aarch64-apple-darwin"]; - break; - - default: - targets = ['x86_64-unknown-linux-musl']; - process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; - process.env["PKG_CONFIG_ALL_STATIC"] = "1"; - break; -} - -targets.forEach(target => { - child_process.execSync(`npm run build -- --target ${target}`, {stdio: 'inherit'}); -}); diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index deaf6b8e57f..fe4ab59fd8e 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -51,3 +51,33 @@ export namespace powermonitors { export function onLock(callback: (err: Error | null, ) => any): Promise export function isLockMonitorAvailable(): Promise } +export namespace ipc { + export interface IpcMessage { + clientId: number + kind: IpcMessageType + message?: string + } + export const enum IpcMessageType { + Connected = 0, + Disconnected = 1, + Message = 2 + } + export class IpcServer { + /** + * Create and start the IPC server without blocking. + * + * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + * @param callback This function will be called whenever a message is received from a client. + */ + static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise + /** Stop the IPC server. */ + stop(): void + /** + * Send a message over the IPC server to all the connected clients + * + * @return The number of clients that the message was sent to. Note that the number of messages + * actually received may be less, as some clients could disconnect before receiving the message. + */ + send(message: string): number + } +} diff --git a/apps/desktop/desktop_native/napi/index.js b/apps/desktop/desktop_native/napi/index.js index 680f1302b9a..a0cfee8e1a0 100644 --- a/apps/desktop/desktop_native/napi/index.js +++ b/apps/desktop/desktop_native/napi/index.js @@ -206,10 +206,4 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { passwords, biometrics, clipboards, processisolations, powermonitors } = nativeBinding - -module.exports.passwords = passwords -module.exports.biometrics = biometrics -module.exports.clipboards = clipboards -module.exports.processisolations = processisolations -module.exports.powermonitors = powermonitors +module.exports = nativeBinding diff --git a/apps/desktop/desktop_native/napi/package.json b/apps/desktop/desktop_native/napi/package.json index 70e472b3952..9f098c4965d 100644 --- a/apps/desktop/desktop_native/napi/package.json +++ b/apps/desktop/desktop_native/napi/package.json @@ -3,9 +3,7 @@ "version": "0.1.0", "description": "", "scripts": { - "build": "napi build --release --platform --js false", - "build:debug": "napi build --platform --js false", - "build:cross-platform": "node build.js", + "build": "napi build --platform --js false", "test": "cargo test" }, "author": "", diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index dfdc316d259..838eb651244 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -189,3 +189,103 @@ pub mod powermonitors { } } + +#[napi] +pub mod ipc { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ + ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + + #[napi(object)] + pub struct IpcMessage { + pub client_id: u32, + pub kind: IpcMessageType, + pub message: Option, + } + + impl From for IpcMessage { + fn from(message: Message) -> Self { + IpcMessage { + client_id: message.client_id, + kind: message.kind.into(), + message: message.message, + } + } + } + + #[napi] + pub enum IpcMessageType { + Connected, + Disconnected, + Message, + } + + impl From for IpcMessageType { + fn from(message_type: MessageType) -> Self { + match message_type { + MessageType::Connected => IpcMessageType::Connected, + MessageType::Disconnected => IpcMessageType::Disconnected, + MessageType::Message => IpcMessageType::Message, + } + } + } + + #[napi] + pub struct IpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl IpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + /// @param callback This function will be called whenever a message is received from a client. + #[napi(factory)] + pub async fn listen( + name: String, + #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] + callback: ThreadsafeFunction, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(message) = recv.recv().await { + callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(IpcServer { server }) + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + /// Send a message over the IPC server to all the connected clients + /// + /// @return The number of clients that the message was sent to. Note that the number of messages + /// actually received may be less, as some clients could disconnect before receiving the message. + #[napi] + pub fn send(&self, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml new file mode 100644 index 00000000000..b15b07a78d3 --- /dev/null +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -0,0 +1,16 @@ +[package] +edition = "2021" +exclude = ["index.node"] +license = "GPL-3.0" +name = "desktop_proxy" +version = "0.0.0" +publish = false + +[dependencies] +anyhow = "=1.0.86" +desktop_core = { path = "../core", default-features = false } +futures = "0.3.30" +log = "0.4.21" +simplelog = "0.12.2" +tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "rt"] } +tokio-util = { version = "0.7.11", features = ["codec"] } diff --git a/apps/desktop/desktop_native/proxy/src/main.rs b/apps/desktop/desktop_native/proxy/src/main.rs new file mode 100644 index 00000000000..d4467c3f1cc --- /dev/null +++ b/apps/desktop/desktop_native/proxy/src/main.rs @@ -0,0 +1,137 @@ +use std::path::Path; + +use desktop_core::ipc::{MESSAGE_CHANNEL_BUFFER, NATIVE_MESSAGING_BUFFER_SIZE}; +use futures::{SinkExt, StreamExt}; +use log::*; +use tokio_util::codec::LengthDelimitedCodec; + +fn init_logging(log_path: &Path, level: log::LevelFilter) { + use simplelog::{ColorChoice, CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode}; + + let config = Config::default(); + + let mut loggers: Vec> = Vec::new(); + loggers.push(TermLogger::new( + level, + config.clone(), + TerminalMode::Stderr, + ColorChoice::Auto, + )); + + match std::fs::File::create(log_path) { + Ok(file) => { + loggers.push(simplelog::WriteLogger::new(level, config, file)); + } + Err(e) => { + eprintln!("Can't create file: {}", e); + } + } + + if let Err(e) = CombinedLogger::init(loggers) { + eprintln!("Failed to initialize logger: {}", e); + } +} + +/// Bitwarden IPC Proxy. +/// +/// This proxy allows browser extensions to communicate with a desktop application using Native +/// Messaging. This method allows an extension to send and receive messages through the use of +/// stdin/stdout streams. +/// +/// However, this also requires the browser to start the process in order for the communication to +/// occur. To overcome this limitation, we implement Inter-Process Communication (IPC) to establish +/// a stable communication channel between the proxy and the running desktop application. +/// +/// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop +/// +#[tokio::main(flavor = "current_thread")] +async fn main() { + let sock_path = desktop_core::ipc::path("bitwarden"); + + let log_path = { + let mut path = sock_path.clone(); + path.set_extension("bitwarden.log"); + path + }; + + init_logging(&log_path, LevelFilter::Info); + + info!("Starting Bitwarden IPC Proxy."); + + // Different browsers send different arguments when the app starts: + // + // Firefox: + // - The complete path to the app manifest. (in the form `/Users//Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`) + // - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in the form `{[UUID]}`). + // + // Chrome on Windows: + // - Origin of the extension that started it (in the form `chrome-extension://[ID]`). + // - Handle to the Chrome native window that started the app. + // + // Chrome on Linux and Mac: + // - Origin of the extension that started it (in the form `chrome-extension://[ID]`). + + let args: Vec<_> = std::env::args().skip(1).collect(); + info!("Process args: {:?}", args); + + // Setup two channels, one for sending messages to the desktop application (`out`) and one for receiving messages from the desktop application (`in`) + let (in_send, in_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER); + let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER); + + let mut handle = tokio::spawn(desktop_core::ipc::client::connect( + sock_path, out_send, in_recv, + )); + + // Create a new codec for reading and writing messages from stdin/stdout. + let mut stdin = LengthDelimitedCodec::builder() + .max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE) + .native_endian() + .new_read(tokio::io::stdin()); + let mut stdout = LengthDelimitedCodec::builder() + .max_frame_length(NATIVE_MESSAGING_BUFFER_SIZE) + .native_endian() + .new_write(tokio::io::stdout()); + + loop { + tokio::select! { + // IPC client has finished, so we should exit as well. + _ = &mut handle => { + break; + } + + // Receive messages from IPC and print to STDOUT. + msg = out_recv.recv() => { + match msg { + Some(msg) => { + debug!("OUT: {}", msg); + stdout.send(msg.into()).await.unwrap(); + } + None => { + info!("Channel closed, exiting."); + break; + } + } + }, + + // Listen to stdin and send messages to ipc processor. + msg = stdin.next() => { + match msg { + Some(Ok(msg)) => { + let m = String::from_utf8(msg.to_vec()).unwrap(); + debug!("IN: {}", m); + in_send.send(m).await.unwrap(); + } + Some(Err(e)) => { + error!("Error parsing input: {}", e); + break; + } + None => { + info!("Received EOF, exiting."); + break; + } + } + } + + } + } +} diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index b6572587faa..02bb2377767 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -73,6 +73,13 @@ "CFBundleDevelopmentRegion": "en" }, "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", + "extraFiles": [ + { + "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}", + "to": "MacOS/desktop_proxy" + } + ], + "signIgnore": ["MacOS/desktop_proxy"], "target": ["dmg", "zip"] }, "win": { @@ -84,16 +91,24 @@ "from": "../../node_modules/regedit/vbs", "to": "regedit/vbs", "filter": ["**/*"] - }, + } + ], + "extraFiles": [ { - "from": "resources/native-messaging.bat", - "to": "native-messaging.bat" + "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", + "to": "desktop_proxy.exe" } ] }, "linux": { "category": "Utility", "synopsis": "A secure and free password manager for all of your devices.", + "extraFiles": [ + { + "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}", + "to": "desktop_proxy" + } + ], "target": ["deb", "freebsd", "rpm", "AppImage", "snap"], "desktop": { "Name": "Bitwarden", diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 12d141f59cd..3343b4b6024 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -18,7 +18,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.1", + "@types/node": "20.16.4", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -106,9 +106,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.4.tgz", + "integrity": "sha512-ioyQ1zK9aGEomJ45zz8S8IdzElyxhvP1RVWnPrXDf6wFaUb+kk1tEcVVJkF7RPGM0VWI7cp5U57oCPIn5iN1qg==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -241,9 +241,9 @@ "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 757ce74b59f..595cb9c2e0e 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -23,7 +23,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.1", + "@types/node": "20.16.4", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 994aba73292..68be97abfe1 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,7 +18,7 @@ "scripts": { "postinstall": "electron-rebuild", "start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", - "build-native": "cd desktop_native/napi && npm run build", + "build-native": "cd desktop_native && node build.js", "build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", diff --git a/apps/desktop/resources/entitlements.desktop_proxy.plist b/apps/desktop/resources/entitlements.desktop_proxy.plist new file mode 100644 index 00000000000..774e274cf73 --- /dev/null +++ b/apps/desktop/resources/entitlements.desktop_proxy.plist @@ -0,0 +1,14 @@ + + + + + com.apple.application-identifier + LTZ2PFU5D6.com.bitwarden.desktop + com.apple.developer.team-identifier + LTZ2PFU5D6 + com.apple.security.application-groups + + LTZ2PFU5D6.com.bitwarden.desktop + + + diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index 5bfeba83a61..d42ade962c3 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -8,6 +8,10 @@ LTZ2PFU5D6 com.apple.security.app-sandbox + com.apple.security.application-groups + + LTZ2PFU5D6.com.bitwarden.desktop + com.apple.security.network.client com.apple.security.files.user-selected.read-write diff --git a/apps/desktop/resources/native-messaging.bat b/apps/desktop/resources/native-messaging.bat deleted file mode 100644 index 45519250dd6..00000000000 --- a/apps/desktop/resources/native-messaging.bat +++ /dev/null @@ -1,7 +0,0 @@ -@echo off -:: Helper script for starting the Native Messaging Proxy on Windows. - -cd ../ -set ELECTRON_RUN_AS_NODE=1 -set ELECTRON_NO_ATTACH_CONSOLE=1 -Bitwarden.exe resources/app.asar %* diff --git a/apps/desktop/scripts/after-pack.js b/apps/desktop/scripts/after-pack.js index e128397e615..7d588988155 100644 --- a/apps/desktop/scripts/after-pack.js +++ b/apps/desktop/scripts/after-pack.js @@ -1,14 +1,22 @@ /* eslint-disable @typescript-eslint/no-var-requires, no-console */ require("dotenv").config(); +const child_process = require("child_process"); const path = require("path"); +const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses"); +const builder = require("electron-builder"); const fse = require("fs-extra"); exports.default = run; async function run(context) { console.log("## After pack"); - console.log(context); + // console.log(context); + + if (context.packager.platform.nodeName !== "darwin" || context.arch === builder.Arch.universal) { + await addElectronFuses(context); + } + if (context.electronPlatformName === "linux") { console.log("Creating memory-protection wrapper script"); const appOutDir = context.appOutDir; @@ -23,4 +31,114 @@ async function run(context) { fse.chmodSync(wrapperBin, "755"); console.log("Copied memory-protection wrapper script"); } + + if (["darwin", "mas"].includes(context.electronPlatformName)) { + const identities = getIdentities(process.env.CSC_NAME ?? ""); + if (identities.length === 0) { + throw new Error("No valid identities found"); + } + const id = identities[0].id; + + console.log("Signing proxy binary before the main bundle, using identity", id); + + const appName = context.packager.appInfo.productFilename; + const appPath = `${context.appOutDir}/${appName}.app`; + const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy"); + + const packageId = "com.bitwarden.desktop"; + const entitlementsName = "entitlements.desktop_proxy.plist"; + const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName); + child_process.execSync( + `codesign -s ${id} -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`, + ); + } +} + +// Partially based on electron-builder code: +// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/macPackager.ts +// https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/codeSign/macCodeSign.ts + +const appleCertificatePrefixes = [ + "Developer ID Application:", + // "Developer ID Installer:", + // "3rd Party Mac Developer Application:", + // "3rd Party Mac Developer Installer:", + "Apple Development:", +]; + +function getIdentities(csc_name) { + const ids = child_process + .execSync("/usr/bin/security find-identity -v -p codesigning") + .toString(); + + return ids + .split("\n") + .filter((line) => { + for (const prefix of appleCertificatePrefixes) { + if (line.includes(prefix)) { + return true; + } + } + return false; + }) + .filter((line) => line.includes(csc_name)) + .map((line) => { + const split = line.trim().split(" "); + const id = split[1]; + const name = split.slice(2).join(" ").replace(/"/g, ""); + return { id, name }; + }); +} + +/** + * @param {import("electron-builder").AfterPackContext} context + */ +async function addElectronFuses(context) { + const platform = context.packager.platform.nodeName; + + const ext = { + darwin: ".app", + win32: ".exe", + linux: "", + }[platform]; + + const IS_LINUX = platform === "linux"; + const executableName = IS_LINUX + ? context.packager.appInfo.productFilename.toLowerCase().replace("-dev", "").replace(" ", "-") + : context.packager.appInfo.productFilename; // .toLowerCase() to accomodate Linux file named `name` but productFileName is `Name` -- Replaces '-dev' because on Linux the executable name is `name` even for the DEV builds + + const electronBinaryPath = path.join(context.appOutDir, `${executableName}${ext}`); + + console.log("## Adding fuses to the electron binary", electronBinaryPath); + + await flipFuses(electronBinaryPath, { + version: FuseVersion.V1, + strictlyRequireAllFuses: true, + resetAdHocDarwinSignature: platform === "darwin" && context.arch === builder.Arch.universal, + + // List of fuses and their default values is available at: + // https://www.electronjs.org/docs/latest/tutorial/fuses + + [FuseV1Options.RunAsNode]: false, + [FuseV1Options.EnableCookieEncryption]: true, + [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, + [FuseV1Options.EnableNodeCliInspectArguments]: false, + + // Currently, asar integrity is only implemented for macOS and Windows + // https://www.electronjs.org/docs/latest/tutorial/asar-integrity + // On macOS, it works by default, but on Windows it requires the + // asarIntegrity feature of electron-builder v25, currently in alpha + // https://github.com/electron-userland/electron-builder/releases/tag/v25.0.0-alpha.10 + [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: platform === "darwin", + + [FuseV1Options.OnlyLoadAppFromAsar]: true, + + // App refuses to open when enabled + [FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false, + + // To disable this, we should stop using the file:// protocol to load the app bundle + // This can be done by defining a custom app:// protocol and loading the bundle from there, + // but then any requests to the server will be blocked by CORS policy + [FuseV1Options.GrantFileProtocolExtraPrivileges]: true, + }); } diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index f641d801b8d..c6dbadade42 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -1,6 +1,6 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; @@ -51,7 +51,7 @@ type InactiveAccount = ActiveAccount & { ]), ], }) -export class AccountSwitcherComponent { +export class AccountSwitcherComponent implements OnInit { activeAccount$: Observable; inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>; authStatus = AuthenticationStatus; @@ -151,6 +151,24 @@ export class AccountSwitcherComponent { ); } + async ngOnInit() { + const active = await firstValueFrom(this.accountService.activeAccount$); + if (active == null) { + return; + } + const authStatus = await firstValueFrom( + this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])), + ); + if (authStatus === AuthenticationStatus.LoggedOut) { + const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$); + if (nextUpAccount != null) { + await this.switch(nextUpAccount.id); + } else { + await this.addAccount(); + } + } + } + toggle() { this.isOpen = !this.isOpen; } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index be110be138b..d4b51ca1c7e 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,6 +1,7 @@ import { APP_INITIALIZER, NgModule } from "@angular/core"; import { Subject, merge } from "rxjs"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, @@ -26,7 +27,6 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; 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"; @@ -285,7 +285,7 @@ const safeProviders: SafeProvider[] = [ KdfConfigService, InternalMasterPasswordServiceAbstraction, OrganizationApiServiceAbstraction, - OrganizationUserService, + OrganizationUserApiService, InternalUserDecryptionOptionsServiceAbstraction, ], }), diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 28f1f69a598..21bc7e8db14 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -1,11 +1,11 @@ import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -50,7 +50,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On private ngZone: NgZone, stateService: StateService, organizationApiService: OrganizationApiServiceAbstraction, - organizationUserService: OrganizationUserService, + organizationUserApiService: OrganizationUserApiService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, @@ -74,7 +74,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On route, stateService, organizationApiService, - organizationUserService, + organizationUserApiService, userDecryptionOptionsService, ssoLoginService, dialogService, diff --git a/apps/desktop/src/entry.ts b/apps/desktop/src/entry.ts index 78fe51e8b9e..3bb84461363 100644 --- a/apps/desktop/src/entry.ts +++ b/apps/desktop/src/entry.ts @@ -1,31 +1,33 @@ -import { NativeMessagingProxy } from "./proxy/native-messaging-proxy"; +import { spawn } from "child_process"; +import * as path from "path"; -// We need to import the other dependencies using `require` since `import` will -// generate `Error: Cannot find module 'electron'`. The cause of this error is -// due to native messaging setting the ELECTRON_RUN_AS_NODE env flag on windows -// which removes the electron module. This flag is needed for stdin/out to work -// properly on Windows. +import { app } from "electron"; if ( + process.platform === "darwin" && process.argv.some((arg) => arg.indexOf("chrome-extension://") !== -1 || arg.indexOf("{") !== -1) ) { - if (process.platform === "darwin") { - // eslint-disable-next-line - const app = require("electron").app; + // If we're on MacOS, we need to support DuckDuckGo's IPC communication, + // which for the moment is launching the Bitwarden process. + // Ideally the browser would instead startup the desktop_proxy process + // when available, but for now we'll just launch it here. - app.on("ready", () => { - app.dock.hide(); - }); - } - - process.stdout.on("error", (e) => { - if (e.code === "EPIPE") { - process.exit(0); - } + app.on("ready", () => { + app.dock.hide(); }); - const proxy = new NativeMessagingProxy(); - proxy.run(); + const proc = spawn(path.join(process.execPath, "..", "desktop_proxy"), process.argv.slice(1), { + cwd: process.cwd(), + stdio: "inherit", + shell: false, + }); + + proc.on("exit", () => { + process.exit(0); + }); + proc.on("error", () => { + process.exit(1); + }); } else { // eslint-disable-next-line const Main = require("./main").Main; diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 86d07440a73..212e5b50ee1 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -220,6 +220,7 @@ export class Main { this.windowMain, app.getPath("userData"), app.getPath("exe"), + app.getAppPath(), ); this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider); @@ -265,13 +266,21 @@ export class Main { if (browserIntegrationEnabled || ddgIntegrationEnabled) { // Re-register the native messaging host integrations on startup, in case they are not present if (browserIntegrationEnabled) { - this.nativeMessagingMain.generateManifests().catch(this.logService.error); + this.nativeMessagingMain + .generateManifests() + .catch((err) => this.logService.error("Error while generating manifests", err)); } if (ddgIntegrationEnabled) { - this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error); + this.nativeMessagingMain + .generateDdgManifests() + .catch((err) => this.logService.error("Error while generating DDG manifests", err)); } - this.nativeMessagingMain.listen(); + this.nativeMessagingMain + .listen() + .catch((err) => + this.logService.error("Error while starting native message listener", err), + ); } app.removeAsDefaultProtocolClient("bitwarden"); diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 8c8404578b6..036f35e61c8 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -1,34 +1,34 @@ import { existsSync, promises as fs } from "fs"; -import { Socket } from "net"; import { homedir, userInfo } from "os"; import * as path from "path"; import * as util from "util"; import { ipcMain } from "electron"; -import * as ipc from "node-ipc"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ipc } from "@bitwarden/desktop-napi"; -import { getIpcSocketRoot } from "../proxy/ipc"; +import { isDev } from "../utils"; import { WindowMain } from "./window.main"; export class NativeMessagingMain { - private connected: Socket[] = []; - private socket: any; + private ipcServer: ipc.IpcServer | null; + private connected: number[] = []; constructor( private logService: LogService, private windowMain: WindowMain, private userPath: string, private exePath: string, + private appPath: string, ) { ipcMain.handle( "nativeMessaging.manifests", async (_event: any, options: { create: boolean }) => { if (options.create) { - this.listen(); try { + await this.listen(); await this.generateManifests(); } catch (e) { this.logService.error("Error generating manifests: " + e); @@ -51,8 +51,8 @@ export class NativeMessagingMain { "nativeMessaging.ddgManifests", async (_event: any, options: { create: boolean }) => { if (options.create) { - this.listen(); try { + await this.listen(); await this.generateDdgManifests(); } catch (e) { this.logService.error("Error generating duckduckgo manifests: " + e); @@ -72,56 +72,46 @@ export class NativeMessagingMain { ); } - listen() { - ipc.config.id = "bitwarden"; - ipc.config.retry = 1500; - const ipcSocketRoot = getIpcSocketRoot(); - if (ipcSocketRoot != null) { - ipc.config.socketRoot = ipcSocketRoot; + async listen() { + if (this.ipcServer) { + this.ipcServer.stop(); } - ipc.serve(() => { - ipc.server.on("message", (data: any, socket: any) => { - this.socket = socket; - this.windowMain.win.webContents.send("nativeMessaging", data); - }); - - ipcMain.on("nativeMessagingReply", (event, msg) => { - if (this.socket != null && msg != null) { - this.send(msg, this.socket); + this.ipcServer = await ipc.IpcServer.listen("bitwarden", (error, msg) => { + switch (msg.kind) { + case ipc.IpcMessageType.Connected: { + this.connected.push(msg.clientId); + this.logService.info("Native messaging client " + msg.clientId + " has connected"); + break; } - }); + case ipc.IpcMessageType.Disconnected: { + const index = this.connected.indexOf(msg.clientId); + if (index > -1) { + this.connected.splice(index, 1); + } - ipc.server.on("connect", (socket: Socket) => { - this.connected.push(socket); - }); - - ipc.server.on("socket.disconnected", (socket, destroyedSocketID) => { - const index = this.connected.indexOf(socket); - if (index > -1) { - this.connected.splice(index, 1); + this.logService.info("Native messaging client " + msg.clientId + " has disconnected"); + break; } - - this.socket = null; - ipc.log("client " + destroyedSocketID + " has disconnected!"); - }); + case ipc.IpcMessageType.Message: + this.windowMain.win.webContents.send("nativeMessaging", JSON.parse(msg.message)); + break; + } }); - ipc.server.start(); - } - - stop() { - ipc.server.stop(); - // Kill all existing connections - this.connected.forEach((socket) => { - if (!socket.destroyed) { - socket.destroy(); + ipcMain.on("nativeMessagingReply", (event, msg) => { + if (msg != null) { + this.send(msg); } }); } - send(message: object, socket: any) { - ipc.server.emit(socket, "message", message); + stop() { + this.ipcServer?.stop(); + } + + send(message: object) { + this.ipcServer?.send(JSON.stringify(message)); } async generateManifests() { @@ -331,11 +321,20 @@ export class NativeMessagingMain { } private binaryPath() { - if (process.platform === "win32") { - return path.join(path.dirname(this.exePath), "resources", "native-messaging.bat"); + const ext = process.platform === "win32" ? ".exe" : ""; + + if (isDev()) { + return path.join( + this.appPath, + "..", + "desktop_native", + "target", + "debug", + `desktop_proxy${ext}`, + ); } - return this.exePath; + return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`); } private getRegeditInstance() { diff --git a/apps/desktop/src/proxy/ipc.ts b/apps/desktop/src/proxy/ipc.ts deleted file mode 100644 index 0160d6bf294..00000000000 --- a/apps/desktop/src/proxy/ipc.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable no-console */ -import { createHash } from "crypto"; -import { existsSync, mkdirSync } from "fs"; -import { homedir } from "os"; -import { join as path_join } from "path"; - -import * as ipc from "node-ipc"; - -export function getIpcSocketRoot(): string | null { - let socketRoot = null; - - switch (process.platform) { - case "darwin": { - const ipcSocketRootDir = path_join(homedir(), "tmp"); - if (!existsSync(ipcSocketRootDir)) { - mkdirSync(ipcSocketRootDir); - } - socketRoot = ipcSocketRootDir + "/"; - break; - } - case "win32": { - // Let node-ipc use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user. - // Hashing prevents problems with reserved characters and file length limitations. - socketRoot = createHash("sha1").update(homedir()).digest("hex") + "."; - } - } - return socketRoot; -} - -ipc.config.id = "proxy"; -ipc.config.retry = 1500; -ipc.config.logger = console.warn; // Stdout is used for native messaging -const ipcSocketRoot = getIpcSocketRoot(); -if (ipcSocketRoot != null) { - ipc.config.socketRoot = ipcSocketRoot; -} - -export default class IPC { - onMessage: (message: object) => void; - - private connected = false; - - connect() { - ipc.connectTo("bitwarden", () => { - ipc.of.bitwarden.on("connect", () => { - this.connected = true; - console.error("## connected to bitwarden desktop ##"); - - // Notify browser extension, connection is established to desktop application. - this.onMessage({ command: "connected" }); - }); - - ipc.of.bitwarden.on("disconnect", () => { - this.connected = false; - console.error("disconnected from world"); - - // Notify browser extension, no connection to desktop application. - this.onMessage({ command: "disconnected" }); - }); - - ipc.of.bitwarden.on("message", (message: any) => { - this.onMessage(message); - }); - - ipc.of.bitwarden.on("error", (err: any) => { - console.error("error", err); - }); - }); - } - - isConnected(): boolean { - return this.connected; - } - - send(json: object) { - ipc.of.bitwarden.emit("message", json); - } -} diff --git a/apps/desktop/src/proxy/native-messaging-proxy.ts b/apps/desktop/src/proxy/native-messaging-proxy.ts deleted file mode 100644 index f1b54a82014..00000000000 --- a/apps/desktop/src/proxy/native-messaging-proxy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import IPC from "./ipc"; -import NativeMessage from "./nativemessage"; - -// Proxy is a lightweight application which provides bi-directional communication -// between the browser extension and a running desktop application. -// -// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop -export class NativeMessagingProxy { - private ipc: IPC; - private nativeMessage: NativeMessage; - - constructor() { - this.ipc = new IPC(); - this.nativeMessage = new NativeMessage(this.ipc); - } - - run() { - this.ipc.connect(); - this.nativeMessage.listen(); - - this.ipc.onMessage = this.nativeMessage.send; - } -} diff --git a/apps/desktop/src/proxy/nativemessage.ts b/apps/desktop/src/proxy/nativemessage.ts deleted file mode 100644 index f7a32296f84..00000000000 --- a/apps/desktop/src/proxy/nativemessage.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable no-console */ -import IPC from "./ipc"; - -// Mostly based on the example from MDN, -// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging -export default class NativeMessage { - ipc: IPC; - - constructor(ipc: IPC) { - this.ipc = ipc; - } - - send(message: object) { - const messageBuffer = Buffer.from(JSON.stringify(message)); - - const headerBuffer = Buffer.alloc(4); - headerBuffer.writeUInt32LE(messageBuffer.length, 0); - - process.stdout.write(Buffer.concat([headerBuffer, messageBuffer])); - } - - listen() { - let payloadSize: number = null; - - // A queue to store the chunks as we read them from stdin. - // This queue can be flushed when `payloadSize` data has been read - const chunks: any = []; - - // Only read the size once for each payload - const sizeHasBeenRead = () => Boolean(payloadSize); - - // All the data has been read, reset everything for the next message - const flushChunksQueue = () => { - payloadSize = null; - chunks.splice(0); - }; - - const processData = () => { - // Create one big buffer with all all the chunks - const stringData = Buffer.concat(chunks); - console.error(stringData); - - // The browser will emit the size as a header of the payload, - // if it hasn't been read yet, do it. - // The next time we'll need to read the payload size is when all of the data - // of the current payload has been read (ie. data.length >= payloadSize + 4) - if (!sizeHasBeenRead()) { - try { - payloadSize = stringData.readUInt32LE(0); - } catch (e) { - console.error(e); - return; - } - } - - // If the data we have read so far is >= to the size advertised in the header, - // it means we have all of the data sent. - // We add 4 here because that's the size of the bytes that old the payloadSize - if (stringData.length >= payloadSize + 4) { - // Remove the header - const contentWithoutSize = stringData.slice(4, payloadSize + 4).toString(); - - // Reset the read size and the queued chunks - flushChunksQueue(); - - const json = JSON.parse(contentWithoutSize); - - // Forward to desktop application - this.ipc.send(json); - } - }; - - process.stdin.on("readable", () => { - // A temporary variable holding the nodejs.Buffer of each - // chunk of data read off stdin - let chunk = null; - - // Read all of the available data - // tslint:disable-next-line:no-conditional-assignment - while ((chunk = process.stdin.read()) !== null) { - chunks.push(chunk); - } - - try { - processData(); - } catch (e) { - console.error(e); - } - }); - - process.stdin.on("end", () => { - process.exit(0); - }); - } -} diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 5ffdb3c2076..19f7b8bf70f 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -10,7 +10,7 @@ "types": [], "baseUrl": ".", "paths": { - "@bitwarden/admin-console": ["../../libs/admin-console/src"], + "@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], diff --git a/apps/web/config/base.json b/apps/web/config/base.json index b9102a769d7..5dc03a4633d 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -12,6 +12,6 @@ }, "flags": { "showPasswordless": false, - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index c8ba07e755e..3faa2926929 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -18,6 +18,6 @@ }, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 3fcd8641b32..44391a7450d 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -21,7 +21,7 @@ ], "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false }, "devFlags": {} } diff --git a/apps/web/config/euprd.json b/apps/web/config/euprd.json index 2d554e57043..72f0c1857d9 100644 --- a/apps/web/config/euprd.json +++ b/apps/web/config/euprd.json @@ -12,6 +12,6 @@ }, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index f03d47fe4ee..ac36b107846 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -28,6 +28,6 @@ ], "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index 121f59ba0b3..7e916a11169 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -8,6 +8,6 @@ }, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true + "enableCipherKeyEncryption": false } } 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 52a522c89da..9741758e1e0 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 @@ -1,11 +1,11 @@ import { Injectable } from "@angular/core"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { + OrganizationUserApiService, OrganizationUserInviteRequest, OrganizationUserUpdateRequest, -} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { OrganizationUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; + OrganizationUserDetailsResponse, +} from "@bitwarden/admin-console/common"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CoreOrganizationModule } from "../core-organization.module"; @@ -15,14 +15,14 @@ import { OrganizationUserAdminView } from "../views/organization-user-admin-view export class UserAdminService { constructor( private configService: ConfigService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, ) {} async get( organizationId: string, organizationUserId: string, ): Promise { - const userResponse = await this.organizationUserService.getOrganizationUser( + const userResponse = await this.organizationUserApiService.getOrganizationUser( organizationId, organizationUserId, { @@ -47,7 +47,11 @@ export class UserAdminService { request.groups = user.groups; request.accessSecretsManager = user.accessSecretsManager; - await this.organizationUserService.putOrganizationUser(user.organizationId, user.id, request); + await this.organizationUserApiService.putOrganizationUser( + user.organizationId, + user.id, + request, + ); } async invite(emails: string[], user: OrganizationUserAdminView): Promise { @@ -59,7 +63,7 @@ export class UserAdminService { request.groups = user.groups; request.accessSecretsManager = user.accessSecretsManager; - await this.organizationUserService.postOrganizationUserInvite(user.organizationId, request); + await this.organizationUserApiService.postOrganizationUserInvite(user.organizationId, request); } private async decryptMany( diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts index 86d1f4ded6b..8988f41487c 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts @@ -1,4 +1,4 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html index d296514e357..68582e8b2ea 100644 --- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html @@ -6,31 +6,27 @@
-
- - - - -
+ + {{ "from" | i18n }} + + - -
- - - - -
+ + {{ "to" | i18n }} + + + + + + + `, +}); + +const dialogAccessItems = itemsFactory(10, AccessItemType.Collection); + +export const Dialog: Story = { + args: { + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + showGroupColumn: true, + columnHeader: "Collection", + selectorLabelText: "Select Collections", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No collections added", + disabled: false, + initialValue: [] as any[], + items: dialogAccessItems, + }, + render, +}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts new file mode 100644 index 00000000000..ec7c378f19c --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts @@ -0,0 +1,64 @@ +import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; +import { Meta, StoryObj } from "@storybook/angular"; + +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, AccessItemValue } from "./access-selector.models"; +import { default as baseComponentDefinition } from "./access-selector.stories"; +import { actionsData, itemsFactory } from "./storybook-utils"; + +/** + * Displays the Access Selector embedded in a reactive form. + */ +export default { + title: "Web/Organizations/Access Selector/Reactive form", + decorators: baseComponentDefinition.decorators, + argTypes: { + formObj: { table: { disable: true } }, + }, +} as Meta; + +type FormObj = { formObj: FormGroup<{ formItems: FormControl }> }; +type Story = StoryObj; + +const fb = new FormBuilder(); + +const render: Story["render"] = (args) => ({ + props: { + items: [], + onSubmit: actionsData.onSubmit, + ...args, + }, + template: ` + + + + +`, +}); + +const sampleMembers = itemsFactory(10, AccessItemType.Member); +const sampleGroups = itemsFactory(6, AccessItemType.Group); + +export const ReactiveForm: Story = { + args: { + formObj: fb.group({ formItems: [[{ id: "1g", type: AccessItemType.Group }]] }), + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + columnHeader: "Groups/Members", + selectorLabelText: "Select groups and members", + selectorHelpText: + "Permissions set for a member will replace permissions set by that member's group", + emptySelectionText: "No members or groups added", + items: sampleGroups.concat(sampleMembers), + }, + render, +}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index d0d05004c4c..429b62ed0cc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -1,4 +1,4 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts index 3e551a84753..095be1df966 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts @@ -1,13 +1,8 @@ import { importProvidersFrom } from "@angular/core"; -import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { action } from "@storybook/addon-actions"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - OrganizationUserStatusType, - OrganizationUserType, -} from "@bitwarden/common/admin-console/enums"; import { AvatarModule, BadgeModule, @@ -21,10 +16,20 @@ import { import { PreloadedEnglishI18nModule } from "../../../../../core/tests"; -import { AccessSelectorComponent } from "./access-selector.component"; -import { AccessItemType, AccessItemView, CollectionPermission } from "./access-selector.models"; +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, AccessItemValue, CollectionPermission } from "./access-selector.models"; +import { actionsData, itemsFactory } from "./storybook-utils"; import { UserTypePipe } from "./user-type.pipe"; +/** + * The Access Selector is used to view and edit: + * - member and group access to collections + * - members assigned to groups + * + * It is highly configurable in order to display these relationships from each perspective. For example, you can + * manage member-group relationships from the perspective of a particular member (showing all their groups) or a + * particular group (showing all its members). + */ export default { title: "Web/Organizations/Access Selector", decorators: [ @@ -49,65 +54,16 @@ export default { providers: [importProvidersFrom(PreloadedEnglishI18nModule)], }), ], - parameters: {}, - argTypes: { - formObj: { table: { disable: true } }, - }, } as Meta; -// TODO: This is a workaround since this story does weird things. -type Story = StoryObj; - -const actionsData = { - onValueChanged: action("onValueChanged"), - onSubmit: action("onSubmit"), -}; - -/** - * Factory to help build semi-realistic looking items - * @param n - The number of items to build - * @param type - Which type to build - */ -const itemsFactory = (n: number, type: AccessItemType) => { - return [...Array(n)].map((_: unknown, id: number) => { - const item: AccessItemView = { - id: id.toString(), - type: type, - } as AccessItemView; - - switch (item.type) { - case AccessItemType.Collection: - item.labelName = item.listName = `Collection ${id}`; - item.id = item.id + "c"; - item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1); - break; - case AccessItemType.Group: - item.labelName = item.listName = `Group ${id}`; - item.id = item.id + "g"; - break; - case AccessItemType.Member: - item.id = item.id + "m"; - item.email = `member${id}@email.com`; - item.status = id % 3 == 0 ? 0 : 2; - item.labelName = item.status == 2 ? `Member ${id}` : item.email; - item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email; - item.role = id % 5; - break; - } - - return item; - }); -}; +type Story = StoryObj; const sampleMembers = itemsFactory(10, AccessItemType.Member); const sampleGroups = itemsFactory(6, AccessItemType.Group); -// TODO: These renders are badly handled but storybook has made it more difficult to use multiple renders in a single story. -const StandaloneAccessSelectorRender = (args: any) => ({ +const render: Story["render"] = (args) => ({ props: { - items: [], valueChanged: actionsData.onValueChanged, - initialValue: [], ...args, }, template: ` @@ -127,49 +83,8 @@ const StandaloneAccessSelectorRender = (args: any) => ({ `, }); -const DialogAccessSelectorRender = (args: any) => ({ - props: { - items: [], - valueChanged: actionsData.onValueChanged, - initialValue: [], - ...args, - }, - template: ` - - Access selector - - - - - - - - - - `, -}); - -const dialogAccessItems = itemsFactory(10, AccessItemType.Collection); - -const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).concat([ +const memberCollectionAccessItems = itemsFactory(5, AccessItemType.Collection).concat([ + // These represent collection access via a group { id: "c1-group1", type: AccessItemType.Collection, @@ -190,25 +105,25 @@ const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).c }, ]); -export const Dialog: Story = { - args: { - permissionMode: "edit", - showMemberRoles: false, - showGroupColumn: true, - columnHeader: "Collection", - selectorLabelText: "Select Collections", - selectorHelpText: "Some helper text describing what this does", - emptySelectionText: "No collections added", - disabled: false, - initialValue: [] as any[], - items: dialogAccessItems, - }, - render: DialogAccessSelectorRender, -}; +// Simulate the current user not having permission to change access to this collection +// TODO: currently the member dialog duplicates the AccessItemValue.permission on the +// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication: +// https://bitwarden.atlassian.net/browse/PM-11590 +memberCollectionAccessItems[4].readonly = true; +memberCollectionAccessItems[4].readonlyPermission = CollectionPermission.Manage; +/** + * Displays a member's collection access. + * + * This is currently used in the **Member dialog -> Collections tab**. Note that it includes collection access that the + * member has via a group. + * + * This is also used in the **Groups dialog -> Collections tab** to show a group's collection access and in this + * case the Group column is hidden. + */ export const MemberCollectionAccess: Story = { args: { - permissionMode: "edit", + permissionMode: PermissionMode.Edit, showMemberRoles: false, showGroupColumn: true, columnHeader: "Collection", @@ -216,22 +131,41 @@ export const MemberCollectionAccess: Story = { selectorHelpText: "Some helper text describing what this does", emptySelectionText: "No collections added", disabled: false, - initialValue: [], + initialValue: [ + { + id: "4c", + type: AccessItemType.Collection, + permission: CollectionPermission.Manage, + }, + { + id: "2c", + type: AccessItemType.Collection, + permission: CollectionPermission.Edit, + }, + ], items: memberCollectionAccessItems, }, - render: StandaloneAccessSelectorRender, + render, }; +/** + * Displays the groups a member is assigned to. + * + * This is currently used in the **Member dialog -> Groups tab**. + */ export const MemberGroupAccess: Story = { args: { - permissionMode: "readonly", + permissionMode: PermissionMode.Hidden, showMemberRoles: false, columnHeader: "Groups", selectorLabelText: "Select Groups", selectorHelpText: "Some helper text describing what this does", emptySelectionText: "No groups added", disabled: false, - initialValue: [{ id: "3g" }, { id: "0g" }], + initialValue: [ + { id: "3g", type: AccessItemType.Group }, + { id: "0g", type: AccessItemType.Group }, + ], items: itemsFactory(4, AccessItemType.Group).concat([ { id: "admin", @@ -241,27 +175,40 @@ export const MemberGroupAccess: Story = { }, ]), }, - render: StandaloneAccessSelectorRender, + render, }; +/** + * Displays the members assigned to a group. + * + * This is currently used in the **Group dialog -> Members tab**. + */ export const GroupMembersAccess: Story = { args: { - permissionMode: "hidden", + permissionMode: PermissionMode.Hidden, showMemberRoles: true, columnHeader: "Members", selectorLabelText: "Select Members", selectorHelpText: "Some helper text describing what this does", emptySelectionText: "No members added", disabled: false, - initialValue: [{ id: "2m" }, { id: "0m" }], + initialValue: [ + { id: "2m", type: AccessItemType.Member }, + { id: "0m", type: AccessItemType.Member }, + ], items: sampleMembers, }, - render: StandaloneAccessSelectorRender, + render, }; +/** + * Displays the members and groups assigned to a collection. + * + * This is currently used in the **Collection dialog -> Access tab**. + */ export const CollectionAccess: Story = { args: { - permissionMode: "edit", + permissionMode: PermissionMode.Edit, showMemberRoles: false, columnHeader: "Groups/Members", selectorLabelText: "Select groups and members", @@ -270,68 +217,38 @@ export const CollectionAccess: Story = { emptySelectionText: "No members or groups added", disabled: false, initialValue: [ - { id: "3g", permission: CollectionPermission.EditExceptPass }, - { id: "0m", permission: CollectionPermission.View }, + { id: "3g", type: AccessItemType.Group, permission: CollectionPermission.EditExceptPass }, + { id: "0m", type: AccessItemType.Member, permission: CollectionPermission.View }, + { id: "7m", type: AccessItemType.Member, permission: CollectionPermission.Manage }, ], - items: sampleGroups.concat(sampleMembers).concat([ - { - id: "admin-group", - type: AccessItemType.Group, - listName: "Admin Group", - labelName: "Admin Group", - readonly: true, - }, - { - id: "admin-member", - type: AccessItemType.Member, - listName: "Admin Member (admin@email.com)", - labelName: "Admin Member", - status: OrganizationUserStatusType.Confirmed, - role: OrganizationUserType.Admin, - email: "admin@email.com", - readonly: true, - }, - ]), - }, - render: StandaloneAccessSelectorRender, -}; - -const fb = new FormBuilder(); - -const ReactiveFormAccessSelectorRender = (args: any) => ({ - props: { - items: [], - onSubmit: actionsData.onSubmit, - ...args, - }, - template: ` -
- - -
-`, -}); - -export const ReactiveForm: Story = { - args: { - formObj: fb.group({ formItems: [[{ id: "1g" }]] }), - permissionMode: "edit", - showMemberRoles: false, - columnHeader: "Groups/Members", - selectorLabelText: "Select groups and members", - selectorHelpText: - "Permissions set for a member will replace permissions set by that member's group", - emptySelectionText: "No members or groups added", items: sampleGroups.concat(sampleMembers), }, - render: ReactiveFormAccessSelectorRender, + render, +}; + +// TODO: currently the collection dialog duplicates the AccessItemValue.permission on the +// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication: +// https://bitwarden.atlassian.net/browse/PM-11590 +const disabledMembers = itemsFactory(3, AccessItemType.Member); +disabledMembers[1].readonlyPermission = CollectionPermission.Manage; +disabledMembers[2].readonlyPermission = CollectionPermission.View; + +const disabledGroups = itemsFactory(2, AccessItemType.Group); +disabledGroups[0].readonlyPermission = CollectionPermission.ViewExceptPass; + +/** + * Displays the members and groups assigned to a collection when the control is in a disabled state. + */ +export const DisabledCollectionAccess: Story = { + args: { + ...CollectionAccess.args, + disabled: true, + items: disabledGroups.concat(disabledMembers), + initialValue: [ + { id: "1m", type: AccessItemType.Member, permission: CollectionPermission.Manage }, + { id: "2m", type: AccessItemType.Member, permission: CollectionPermission.View }, + { id: "0g", type: AccessItemType.Group, permission: CollectionPermission.ViewExceptPass }, + ], + }, + render, }; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts new file mode 100644 index 00000000000..fb8bdef1d8c --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts @@ -0,0 +1,44 @@ +import { action } from "@storybook/addon-actions"; + +import { AccessItemType, AccessItemView } from "./access-selector.models"; + +export const actionsData = { + onValueChanged: action("onValueChanged"), + onSubmit: action("onSubmit"), +}; + +/** + * Factory to help build semi-realistic looking items + * @param n - The number of items to build + * @param type - Which type to build + */ +export const itemsFactory = (n: number, type: AccessItemType) => { + return [...Array(n)].map((_: unknown, id: number) => { + const item: AccessItemView = { + id: id.toString(), + type: type, + } as AccessItemView; + + switch (item.type) { + case AccessItemType.Collection: + item.labelName = item.listName = `Collection ${id}`; + item.id = item.id + "c"; + item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1); + break; + case AccessItemType.Group: + item.labelName = item.listName = `Group ${id}`; + item.id = item.id + "g"; + break; + case AccessItemType.Member: + item.id = item.id + "m"; + item.email = `member${id}@email.com`; + item.status = id % 3 == 0 ? 0 : 2; + item.labelName = item.status == 2 ? `Member ${id}` : item.email; + item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email; + item.role = id % 5; + break; + } + + return item; + }); +}; diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index bbd344e289e..17e608df3ee 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -1,6 +1,8 @@ +import { + OrganizationUserApiService, + OrganizationUserResetPasswordEnrollmentRequest, +} from "@bitwarden/admin-console/common"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; @@ -23,7 +25,7 @@ export class EnrollMasterPasswordReset { dialogService: DialogService, data: EnrollMasterPasswordResetData, resetPasswordService: OrganizationUserResetPasswordService, - organizationUserService: OrganizationUserService, + organizationUserApiService: OrganizationUserApiService, platformUtilsService: PlatformUtilsService, i18nService: I18nService, syncService: SyncService, @@ -50,7 +52,7 @@ export class EnrollMasterPasswordReset { // Process the enrollment request, which is an endpoint that is // gated by a server-side check of the master password hash - await organizationUserService.putOrganizationUserResetPasswordEnrollment( + await organizationUserApiService.putOrganizationUserResetPasswordEnrollment( data.organization.id, data.organization.userId, request, diff --git a/apps/web/src/app/auth/key-rotation/request/update-key.request.ts b/apps/web/src/app/auth/key-rotation/request/update-key.request.ts index 9ea40c88e6e..0988ed54a99 100644 --- a/apps/web/src/app/auth/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/auth/key-rotation/request/update-key.request.ts @@ -1,4 +1,4 @@ -import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; 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 a9727532051..2c803a627f3 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 @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index 97a17a5997f..13b704b5466 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -1,9 +1,9 @@ import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { MockProxy, mock } from "jest-mock-extended"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -35,7 +35,7 @@ describe("AcceptOrganizationInviteService", () => { let policyService: MockProxy; let logService: MockProxy; let organizationApiService: MockProxy; - let organizationUserService: MockProxy; + let organizationUserApiService: MockProxy; let i18nService: MockProxy; let globalStateProvider: FakeGlobalStateProvider; let globalState: FakeGlobalState; @@ -49,7 +49,7 @@ describe("AcceptOrganizationInviteService", () => { policyService = mock(); logService = mock(); organizationApiService = mock(); - organizationUserService = mock(); + organizationUserApiService = mock(); i18nService = mock(); globalStateProvider = new FakeGlobalStateProvider(); globalState = globalStateProvider.getFake(ORGANIZATION_INVITE); @@ -63,7 +63,7 @@ describe("AcceptOrganizationInviteService", () => { policyService, logService, organizationApiService, - organizationUserService, + organizationUserApiService, i18nService, globalStateProvider, ); @@ -85,10 +85,10 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAcceptInit).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled(); expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(globalState.nextMock).toHaveBeenCalledWith(null); - expect(organizationUserService.postOrganizationUserAccept).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); @@ -133,10 +133,10 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(globalState.nextMock).toHaveBeenCalledWith(null); - expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); @@ -161,8 +161,8 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); - expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index d1ffa61f6a9..a7798d480fb 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -1,13 +1,13 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, firstValueFrom, map } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { + OrganizationUserApiService, OrganizationUserAcceptRequest, OrganizationUserAcceptInitRequest, -} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +} from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -58,7 +58,7 @@ export class AcceptOrganizationInviteService { private readonly policyService: PolicyService, private readonly logService: LogService, private readonly organizationApiService: OrganizationApiServiceAbstraction, - private readonly organizationUserService: OrganizationUserService, + private readonly organizationUserApiService: OrganizationUserApiService, private readonly i18nService: I18nService, private readonly globalStateProvider: GlobalStateProvider, ) { @@ -121,7 +121,7 @@ export class AcceptOrganizationInviteService { private async acceptAndInitOrganization(invite: OrganizationInvite): Promise { await this.prepareAcceptAndInitRequest(invite).then((request) => - this.organizationUserService.postOrganizationUserAcceptInit( + this.organizationUserApiService.postOrganizationUserAcceptInit( invite.organizationId, invite.organizationUserId, request, @@ -156,7 +156,7 @@ export class AcceptOrganizationInviteService { private async accept(invite: OrganizationInvite): Promise { await this.prepareAcceptRequest(invite).then((request) => - this.organizationUserService.postOrganizationUserAccept( + this.organizationUserApiService.postOrganizationUserAccept( invite.organizationId, invite.organizationUserId, request, diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor-setup.component.ts index 8be6c7a3cc2..3b8a9edd955 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.ts @@ -220,7 +220,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.dialogService, { data: result }, ); - this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onChangeStatus + this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onUpdated .pipe(first(), takeUntil(this.destroy$)) .subscribe((enabled: boolean) => { webAuthnComp.close(); diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts index 9aeafaf2c65..6dfee920991 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts @@ -1,5 +1,5 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, EventEmitter, Inject, NgZone, Output } from "@angular/core"; +import { Component, Inject, NgZone } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -33,7 +33,6 @@ interface Key { templateUrl: "two-factor-webauthn.component.html", }) export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { - @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.WebAuthn; name: string; keys: Key[]; @@ -85,34 +84,33 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { // Should never happen. return Promise.reject(); } + return this.enable(); + }; + + protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest); request.deviceResponse = this.webAuthnResponse; request.id = this.keyIdAvailable; request.name = this.formGroup.value.name; - return this.enableWebAuth(request); - }; - - private enableWebAuth(request: any) { - return super.enable(async () => { - this.formPromise = this.apiService.putTwoFactorWebAuthn(request); - const response = await this.formPromise; - this.processResponse(response); + const response = await this.apiService.putTwoFactorWebAuthn(request); + this.processResponse(response); + this.toastService.showToast({ + title: this.i18nService.t("success"), + message: this.i18nService.t("twoFactorProviderEnabled"), + variant: "success", }); + this.onUpdated.emit(response.enabled); } disable = async () => { - await this.disableWebAuth(); + await this.disableMethod(); if (!this.enabled) { - this.onChangeStatus.emit(this.enabled); + this.onUpdated.emit(this.enabled); this.dialogRef.close(); } }; - private async disableWebAuth() { - return super.disable(this.formPromise); - } - async remove(key: Key) { if (this.keysConfiguredCount <= 1 || key.removePromise != null) { return; @@ -208,7 +206,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { } } this.enabled = response.enabled; - this.onChangeStatus.emit(this.enabled); + this.onUpdated.emit(this.enabled); } static open( diff --git a/apps/web/src/app/auth/two-factor.component.html b/apps/web/src/app/auth/two-factor.component.html index a941413dbb0..b78747e04c2 100644 --- a/apps/web/src/app/auth/two-factor.component.html +++ b/apps/web/src/app/auth/two-factor.component.html @@ -36,7 +36,7 @@ - {{ "verificationCode" | i18n }} + {{ "verificationCode" | i18n }} diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 2d02cbc5bdf..113d2feabe4 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -5,6 +5,8 @@ import { firstValueFrom, lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,10 +14,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { + AdjustStorageDialogV2Component, + AdjustStorageDialogV2ResultType, +} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogResult, openAdjustStorageDialog, -} from "../shared/adjust-storage.component"; +} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -38,6 +44,10 @@ export class UserSubscriptionComponent implements OnInit { cancelPromise: Promise; reinstatePromise: Promise; + protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -49,6 +59,7 @@ export class UserSubscriptionComponent implements OnInit { private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, + private configService: ConfigService, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -150,15 +161,33 @@ export class UserSubscriptionComponent implements OnInit { }; adjustStorage = async (add: boolean) => { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: 4, - add: add, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); + const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); + + if (deprecateStripeSourcesAPI) { + const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { + data: { + price: 4, + cadence: "year", + type: add ? "Add" : "Remove", + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogV2ResultType.Submitted) { + await this.load(); + } + } else { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: 4, + add: add, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } } }; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index f28933a4ecc..2a565face75 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -18,10 +18,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { + AdjustStorageDialogV2Component, + AdjustStorageDialogV2ResultType, +} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogResult, openAdjustStorageDialog, -} from "../shared/adjust-storage.component"; +} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -71,6 +75,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy FeatureFlag.EnableUpgradePasswordManagerSub, ); + protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -458,17 +466,36 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy adjustStorage = (add: boolean) => { return async () => { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: this.storageGbPrice, - add: add, - organizationId: this.organizationId, - interval: this.billingInterval, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); + const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); + + if (deprecateStripeSourcesAPI) { + const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { + data: { + price: this.storageGbPrice, + cadence: this.billingInterval, + type: add ? "Add" : "Remove", + organizationId: this.organizationId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogV2ResultType.Submitted) { + await this.load(); + } + } else { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: this.storageGbPrice, + add: add, + organizationId: this.organizationId, + interval: this.billingInterval, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } } }; }; diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html new file mode 100644 index 00000000000..7b74379acb6 --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html @@ -0,0 +1,34 @@ +
+ + +

{{ body }}

+
+ + {{ storageFieldLabel }} + + + + {{ "total" | i18n }} + {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = + {{ this.price * this.formGroup.value.storage | currency: "$" }} / + {{ this.cadence | i18n }} + + +
+
+ + + + +
+
diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts new file mode 100644 index 00000000000..23d5e46fa1b --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts @@ -0,0 +1,104 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +export interface AdjustStorageDialogV2Params { + price: number; + cadence: "month" | "year"; + type: "Add" | "Remove"; + organizationId?: string; +} + +export enum AdjustStorageDialogV2ResultType { + Submitted = "submitted", + Closed = "closed", +} + +@Component({ + templateUrl: "./adjust-storage-dialog-v2.component.html", +}) +export class AdjustStorageDialogV2Component { + protected formGroup = new FormGroup({ + storage: new FormControl(0, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + }); + + protected organizationId?: string; + protected price: number; + protected cadence: "month" | "year"; + + protected title: string; + protected body: string; + protected storageFieldLabel: string; + + protected ResultType = AdjustStorageDialogV2ResultType; + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params, + private dialogRef: DialogRef, + private i18nService: I18nService, + private organizationApiService: OrganizationApiServiceAbstraction, + private toastService: ToastService, + ) { + this.price = this.dialogParams.price; + this.cadence = this.dialogParams.cadence; + this.organizationId = this.dialogParams.organizationId; + switch (this.dialogParams.type) { + case "Add": + this.title = this.i18nService.t("addStorage"); + this.body = this.i18nService.t("storageAddNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); + break; + case "Remove": + this.title = this.i18nService.t("removeStorage"); + this.body = this.i18nService.t("storageRemoveNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); + break; + } + } + + submit = async () => { + const request = new StorageRequest(); + switch (this.dialogParams.type) { + case "Add": + request.storageGbAdjustment = this.formGroup.value.storage; + break; + case "Remove": + request.storageGbAdjustment = this.formGroup.value.storage * -1; + break; + } + + if (this.organizationId) { + await this.organizationApiService.updateStorage(this.organizationId, request); + } else { + await this.apiService.postAccountStorage(request); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + }); + + this.dialogRef.close(this.ResultType.Submitted); + }; + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open( + AdjustStorageDialogV2Component, + dialogConfig, + ); +} diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html similarity index 100% rename from apps/web/src/app/billing/shared/adjust-storage.component.html rename to apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts similarity index 95% rename from apps/web/src/app/billing/shared/adjust-storage.component.ts rename to apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index 5cf05ea015c..a67c63a9fad 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -12,7 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PaymentComponent } from "./payment/payment.component"; +import { PaymentComponent } from "../payment/payment.component"; export interface AdjustStorageDialogData { storageGbPrice: number; @@ -27,9 +27,9 @@ export enum AdjustStorageDialogResult { } @Component({ - templateUrl: "adjust-storage.component.html", + templateUrl: "adjust-storage-dialog.component.html", }) -export class AdjustStorageComponent { +export class AdjustStorageDialogComponent { storageGbPrice: number; add: boolean; organizationId: string; @@ -126,5 +126,5 @@ export function openAdjustStorageDialog( dialogService: DialogService, config: DialogConfig, ) { - return dialogService.open(AdjustStorageComponent, config); + return dialogService.open(AdjustStorageDialogComponent, config); } diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 300817bad55..c9b3f2de855 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -6,7 +6,8 @@ import { SharedModule } from "../../shared"; import { AddCreditDialogComponent } from "./add-credit-dialog.component"; import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component"; import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { AdjustStorageComponent } from "./adjust-storage.component"; +import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component"; +import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; import { PaymentV2Component } from "./payment/payment-v2.component"; @@ -30,7 +31,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac declarations: [ AddCreditDialogComponent, AdjustPaymentDialogComponent, - AdjustStorageComponent, + AdjustStorageDialogComponent, BillingHistoryComponent, PaymentMethodComponent, SecretsManagerSubscribeComponent, @@ -38,12 +39,13 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac UpdateLicenseDialogComponent, OffboardingSurveyComponent, AdjustPaymentDialogV2Component, + AdjustStorageDialogV2Component, ], exports: [ SharedModule, PaymentComponent, TaxInfoComponent, - AdjustStorageComponent, + AdjustStorageDialogComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 2c0a0471a1f..460af8623e6 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common"; import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, @@ -24,7 +25,6 @@ import { import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; @@ -203,7 +203,7 @@ const safeProviders: SafeProvider[] = [ KdfConfigService, InternalMasterPasswordServiceAbstraction, OrganizationApiServiceAbstraction, - OrganizationUserService, + OrganizationUserApiService, InternalUserDecryptionOptionsServiceAbstraction, ], }), diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index c8cbd9f8dab..7cba19b29ad 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -36,13 +36,13 @@
{{ "loggedInAs" | i18n }} - + {{ account | userName }}
diff --git a/apps/web/src/app/settings/domain-rules.component.html b/apps/web/src/app/settings/domain-rules.component.html index 18091bcc3aa..1c8e5e435ec 100644 --- a/apps/web/src/app/settings/domain-rules.component.html +++ b/apps/web/src/app/settings/domain-rules.component.html @@ -18,7 +18,7 @@ *ngFor="let d of custom; let i = index; trackBy: indexTrackBy" > - {{ "customDomainX" | i18n: i + 1 }} + {{ "customDomainX" | i18n: i + 1 }} - + diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts index 028417faf16..6ab2795afd9 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts @@ -13,6 +13,8 @@ import { IconButtonModule, } from "@bitwarden/components"; +import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; + @Component({ selector: "app-card-details-view", templateUrl: "card-details-view.component.html", @@ -26,6 +28,7 @@ import { TypographyModule, FormFieldModule, IconButtonModule, + ReadOnlyCipherCardComponent, ], }) export class CardDetailsComponent { diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index cb8f86b9809..10701083b79 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -8,10 +8,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { CollectionId } from "@bitwarden/common/types/guid"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { isCardExpired } from "@bitwarden/common/vault/utils"; import { SearchModule, CalloutModule } from "@bitwarden/components"; import { AdditionalOptionsComponent } from "./additional-options/additional-options.component"; @@ -61,7 +61,7 @@ export class CipherViewComponent implements OnInit, OnDestroy { async ngOnInit() { await this.loadCipherData(); - this.cardIsExpired = this.isCardExpiryInThePast(); + this.cardIsExpired = isCardExpired(this.cipher.card); } ngOnDestroy(): void { @@ -70,8 +70,8 @@ export class CipherViewComponent implements OnInit, OnDestroy { } get hasCard() { - const { cardholderName, code, expMonth, expYear, brand, number } = this.cipher.card; - return cardholderName || code || expMonth || expYear || brand || number; + const { cardholderName, code, expMonth, expYear, number } = this.cipher.card; + return cardholderName || code || expMonth || expYear || number; } get hasLogin() { @@ -102,24 +102,4 @@ export class CipherViewComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroyed$)); } } - - isCardExpiryInThePast() { - if (this.cipher.card) { - const { expMonth, expYear }: CardView = this.cipher.card; - - if (expYear && expMonth) { - // `Date` months are zero-indexed - const parsedMonth = parseInt(expMonth) - 1; - const parsedYear = parseInt(expYear); - - // First day of the next month minus one, to get last day of the card month - const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0); - const now = new Date(); - - return cardExpiry < now; - } - } - - return false; - } } diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html index d4c29cf262b..96cb63fe39b 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -8,7 +8,7 @@ *ngFor="let field of fields; let last = last" [ngClass]="{ 'tw-mb-4': !last }" > - + {{ field.name }} - + {{ field.name }} @@ -45,7 +45,7 @@ /> {{ field.name }} - + {{ "linked" | i18n }}: {{ field.name }} {{ "itemName" | i18n }} @@ -22,7 +25,7 @@
  • -
      +
      • {{ "loginCredentials" | i18n }}

        - + {{ "username" | i18n }} @@ -132,5 +132,5 @@ class="disabled:tw-cursor-default" > - + diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts index 3973a666847..6f572f31e87 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -19,6 +19,7 @@ import { } from "@bitwarden/components"; import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component"; +import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; type TotpCodeValues = { totpCode: string; @@ -41,6 +42,7 @@ type TotpCodeValues = { BadgeModule, ColorPasswordModule, BitTotpCountdownComponent, + ReadOnlyCipherCardComponent, ], }) export class LoginCredentialsViewComponent { diff --git a/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.html b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.html new file mode 100644 index 00000000000..65061e818cb --- /dev/null +++ b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.html @@ -0,0 +1,3 @@ + + + diff --git a/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts new file mode 100644 index 00000000000..ed16f3a7cc0 --- /dev/null +++ b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts @@ -0,0 +1,26 @@ +import { AfterViewInit, Component, ContentChildren, QueryList } from "@angular/core"; + +import { CardComponent, BitFormFieldComponent } from "@bitwarden/components"; + +@Component({ + selector: "read-only-cipher-card", + templateUrl: "./read-only-cipher-card.component.html", + standalone: true, + imports: [CardComponent], +}) +/** + * A thin wrapper around the `bit-card` component that disables the bottom border for the last form field. + */ +export class ReadOnlyCipherCardComponent implements AfterViewInit { + @ContentChildren(BitFormFieldComponent) formFields: QueryList; + + ngAfterViewInit(): void { + // Disable the bottom border for the last form field + if (this.formFields.last) { + // Delay model update until next change detection cycle + setTimeout(() => { + this.formFields.last.disableReadOnlyBorder = true; + }); + } + } +} diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html index d12a729f99a..29ccd5daa6b 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.html @@ -3,7 +3,7 @@

        {{ "personalDetails" | i18n }}

        - + {{ "name" | i18n }} @@ -43,7 +43,7 @@ [valueLabel]="'company' | i18n" > - + @@ -51,7 +51,7 @@

        {{ "identification" | i18n }}

        - + {{ "ssn" | i18n }} @@ -111,7 +111,7 @@ [valueLabel]="'licenseNumber' | i18n" > - +
        @@ -119,7 +119,7 @@

        {{ "contactInfo" | i18n }}

        - + {{ "email" | i18n }} @@ -166,5 +166,5 @@ [valueLabel]="'address' | i18n" > - +
        diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts index 0fd2c292952..0f3a9f89712 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts @@ -12,6 +12,8 @@ import { TypographyModule, } from "@bitwarden/components"; +import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; + @Component({ standalone: true, selector: "app-view-identity-sections", @@ -25,6 +27,7 @@ import { TypographyModule, FormFieldModule, IconButtonModule, + ReadOnlyCipherCardComponent, ], }) export class ViewIdentitySectionsComponent implements OnInit { diff --git a/package-lock.json b/package-lock.json index 54e3a5cc6a7..73149dded8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", + "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "@microsoft/signalr": "8.0.7", @@ -108,7 +109,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.16.1", + "@types/node": "20.16.4", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -358,14 +359,14 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.1.tgz", - "integrity": "sha512-XTnJfCBMDQl3xF4w/eNrq821gbj2Ig1cqbzpRflhz4pqrANTAfHfPoIC7piWEZ60FNlHapzb6fvh6tJUGXG9og==", + "version": "0.1802.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.2.tgz", + "integrity": "sha512-LPRl9jhcf0NgshaL6RoUy1uL/cAyNt7oxctoZ9EHUu8eh5E9W/jZGhVowjOLpirwqYhmEzKJJIeS49Ssqs3RQg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@angular-devkit/core": "18.2.1", + "@angular-devkit/core": "18.2.2", "rxjs": "7.8.1" }, "engines": { @@ -1439,9 +1440,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.1.tgz", - "integrity": "sha512-fSuGj6CxiTFR+yjuVcaWqaVb5Wts39CSBYRO1BlsOlbuWFZ2NKC/BAb5bdxpB31heCBJi7e3XbPvcMMJIcnKlA==", + "version": "18.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.2.tgz", + "integrity": "sha512-Zz0tGptI/QQnUBDdp+1G5wGwQWMjpfe2oO+UohkrDVgFS71yVj4VDnOy51kMTxBvzw+36evTgthPpmzqPIfxBw==", "dev": true, "license": "MIT", "peer": true, @@ -1947,13 +1948,13 @@ } }, "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { - "version": "7.25.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", - "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -2146,12 +2147,12 @@ } }, "node_modules/@babel/core/node_modules/@babel/generator": { - "version": "7.25.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", - "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -2587,13 +2588,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "license": "MIT", "dependencies": { "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -2700,12 +2701,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", - "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -2948,13 +2949,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", + "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2964,13 +2965,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -4490,16 +4491,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", - "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.4", - "@babel/parser": "^7.25.4", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.4", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -4508,12 +4509,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.25.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", - "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -4537,9 +4538,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", - "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.24.8", @@ -5100,6 +5101,33 @@ "node": "*" } }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -8081,9 +8109,9 @@ } }, "node_modules/@storybook/angular/node_modules/@types/node": { - "version": "18.19.47", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", - "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "version": "18.19.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", + "integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -8186,9 +8214,9 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { - "version": "18.19.47", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", - "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "version": "18.19.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", + "integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -8395,9 +8423,9 @@ } }, "node_modules/@storybook/core-webpack/node_modules/@types/node": { - "version": "18.19.47", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", - "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "version": "18.19.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", + "integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -8412,9 +8440,9 @@ "license": "MIT" }, "node_modules/@storybook/core/node_modules/@types/node": { - "version": "18.19.47", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", - "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "version": "18.19.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.48.tgz", + "integrity": "sha512-7WevbG4ekUcRQSZzOwxWgi5dZmTak7FaxXDoW7xVxPBmKx1rTzfmRLkeCgJzcbBnOV2dkhAPc8cCeT6agocpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -9401,9 +9429,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.4.tgz", + "integrity": "sha512-ioyQ1zK9aGEomJ45zz8S8IdzElyxhvP1RVWnPrXDf6wFaUb+kk1tEcVVJkF7RPGM0VWI7cp5U57oCPIn5iN1qg==", "dev": true, "license": "MIT", "dependencies": { @@ -9517,9 +9545,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", - "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", + "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "dev": true, "license": "MIT", "dependencies": { @@ -12380,9 +12408,9 @@ } }, "node_modules/axios": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", - "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13624,9 +13652,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001653", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz", - "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==", + "version": "1.0.30001655", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", + "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", "funding": [ { "type": "opencollective", @@ -17129,9 +17157,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -18917,9 +18945,9 @@ "license": "ISC" }, "node_modules/flow-parser": { - "version": "0.244.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.244.0.tgz", - "integrity": "sha512-Dkc88m5k8bx1VvHTO9HEJ7tvMcSb3Zvcv1PY4OHK7pHdtdY2aUjhmPy6vpjVJ2uUUOIybRlb91sXE8g4doChtA==", + "version": "0.245.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.245.0.tgz", + "integrity": "sha512-xUBkkpIDfDZHAebnDEX65FCVitJUctab82KFmtP5SY4cGly1vbuYNe6Muyp0NLXrgmBChVdoC2T+3/RUHi4Mww==", "dev": true, "license": "MIT", "engines": { @@ -19656,9 +19684,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", - "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz", + "integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==", "dev": true, "license": "MIT", "dependencies": { @@ -21732,9 +21760,9 @@ } }, "node_modules/i18next/node_modules/@babel/runtime": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz", - "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -35190,9 +35218,9 @@ } }, "node_modules/streamx": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.19.0.tgz", - "integrity": "sha512-5z6CNR4gtkPbwlxyEqoDGDmWIzoNJqCBt4Eac1ICP9YaIT08ct712cFj0u1rx4F8luAuL+3Qc+RFIdI4OX00kg==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.0.tgz", + "integrity": "sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37224,9 +37252,9 @@ "license": "MIT" }, "node_modules/uglify-js": { - "version": "3.19.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", - "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, "license": "BSD-2-Clause", "optional": true, @@ -37589,14 +37617,13 @@ } }, "node_modules/unplugin": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.12.2.tgz", - "integrity": "sha512-bEqQxeC7rxtxPZ3M5V4Djcc4lQqKPgGe3mAWZvxcSmX5jhGxll19NliaRzQSQPrk4xJZSGniK3puLWpRuZN7VQ==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.12.3.tgz", + "integrity": "sha512-my8DH0/T/Kx33KO+6QXAqdeMYgyy0GktlOpdQjpagfHKw5DrD0ctPr7SHUyOT3g4ZVpzCQGt/qcpuoKJ/pniHA==", "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.12.1", - "chokidar": "^3.6.0", "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.6.2" }, @@ -37604,31 +37631,6 @@ "node": ">=14.0.0" } }, - "node_modules/unplugin/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, - "license": "MIT", - "dependencies": { - "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/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", diff --git a/package.json b/package.json index 722fd6c3e00..872849eb851 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.16.1", + "@types/node": "20.16.4", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -157,6 +157,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", + "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "12.0.1", "@microsoft/signalr": "8.0.7", diff --git a/tsconfig.json b/tsconfig.json index 79edf0da2e1..46829a9c30f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "baseUrl": ".", "resolveJsonModule": true, "paths": { - "@bitwarden/admin-console": ["./libs/admin-console/src"], + "@bitwarden/admin-console/common": ["./libs/admin-console/src/common"], "@bitwarden/angular/*": ["./libs/angular/src/*"], "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/auth/angular": ["./libs/auth/src/angular"],