diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 93693f183c..c050ee1f6c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -71,6 +71,7 @@ bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev ## Platform team files ## apps/browser/src/platform @bitwarden/team-platform-dev apps/cli/src/platform @bitwarden/team-platform-dev +apps/desktop/macos @bitwarden/team-platform-dev apps/desktop/src/platform @bitwarden/team-platform-dev apps/web/src/app/platform @bitwarden/team-platform-dev libs/angular/src/platform @bitwarden/team-platform-dev @@ -91,6 +92,7 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev apps/browser/src/autofill @bitwarden/team-autofill-dev apps/desktop/src/autofill @bitwarden/team-autofill-dev libs/common/src/autofill @bitwarden/team-autofill-dev +apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev apps/desktop/src/services/native-message-handler.service.ts @bitwarden/team-autofill-dev diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 221c998247..15ef794610 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -166,7 +166,7 @@ jobs: - name: Set up environment run: | sudo apt-get update - sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm musl-dev musl-tools + sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev rpm musl-dev musl-tools flatpak flatpak-builder - name: Set up Snap run: sudo snap install snapcraft --classic @@ -248,6 +248,19 @@ jobs: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml if-no-files-found: error + + - name: Build flatpak + working-directory: apps/desktop + run: | + sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + sudo npm run pack:lin:flatpak + + - name: Upload flatpak artifact + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: com.bitwarden.desktop.flatpak + path: apps/desktop/dist/com.bitwarden.desktop.flatpak + if-no-files-found: error windows: @@ -1164,6 +1177,21 @@ jobs: --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ --output none + az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + --name bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --file $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --output none + + - name: Set up provisioning profiles + run: | + AUTOFILL_PROFILE_PATH=$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile + PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles + + mkdir -p "$PROFILES_DIR_PATH" + + AUTOFILL_UUID=$(grep UUID -A1 -a $AUTOFILL_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") + cp $AUTOFILL_PROFILE_PATH "$PROFILES_DIR_PATH/$AUTOFILL_UUID.provisionprofile" + - name: Get certificates run: | mkdir -p $HOME/certificates @@ -1215,11 +1243,6 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - - name: Set up provisioning profiles - run: | - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile - - name: Increment version shell: pwsh env: diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index ba4f2599f3..7e89a5c7cd 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -216,7 +216,7 @@ jobs: - name: Generate Docker image tag id: tag run: | - if [[ $(grep "pull" <<< "${GITHUB_REF}") ]]; then + if [[ "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") else IMAGE_TAG=$(echo "${GITHUB_REF_NAME}" | sed "s#/#-#g") diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 561cd9af0c..9dc72c7fdd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,6 +36,7 @@ jobs: ! -path "./.github/*" \ ! -path "*/Cargo.toml" \ ! -path "*/Cargo.lock" \ + ! -path "./apps/desktop/macos/*" \ > tmp.txt diff <(sort .github/whitelist-capital-letters.txt) <(sort tmp.txt) diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 21de47f13b..9935ef7674 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -44,7 +44,6 @@ jobs: runs-on: ubuntu-24.04 outputs: branch: ${{ steps.set-branch.outputs.branch }} - token: ${{ steps.app-token.outputs.token }} steps: - name: Set branch id: set-branch @@ -59,13 +58,6 @@ jobs: echo "branch=$BRANCH" >> $GITHUB_OUTPUT - - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 - id: app-token - with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} - cut_branch: name: Cut branch @@ -73,11 +65,18 @@ jobs: needs: setup runs-on: ubuntu-24.04 steps: + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Check out target ref uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.target_ref }} - token: ${{ needs.setup.outputs.token }} + token: ${{ steps.app-token.outputs.token }} - name: Check if ${{ needs.setup.outputs.branch }} branch exists env: @@ -115,11 +114,18 @@ jobs: with: version: ${{ inputs.version_number_override }} + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Check out branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main - token: ${{ needs.setup.outputs.token }} + token: ${{ steps.app-token.outputs.token }} - name: Configure Git run: | @@ -445,11 +451,18 @@ jobs: - bump_version - setup steps: + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Check out main branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main - token: ${{ needs.setup.outputs.token }} + token: ${{ steps.app-token.outputs.token }} - name: Configure Git run: | diff --git a/.gitignore b/.gitignore index 6dea4b43f1..d0d8edd596 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,9 @@ npm-debug.log dist build .angular/cache +.flatpak +.flatpak-repo +.flatpak-builder # Testing coverage diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 4a206b36fa..2589a08da1 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -1,4 +1,4 @@ - + diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fb92ebe04a..fdfd5740ba 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -376,6 +376,7 @@ export default class MainBackground { autoSubmitLoginBackground: AutoSubmitLoginBackground; sdkService: SdkService; cipherAuthorizationService: CipherAuthorizationService; + inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -1249,6 +1250,8 @@ export default class MainBackground { this.collectionService, this.organizationService, ); + + this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); } async bootstrap() { @@ -1630,7 +1633,6 @@ export default class MainBackground { this.themeStateService, ); } else { - const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); this.overlayBackground = new OverlayBackground( this.logService, this.cipherService, @@ -1643,7 +1645,7 @@ export default class MainBackground { this.platformUtilsService, this.vaultSettingsService, this.fido2ActiveRequestManager, - inlineMenuFieldQualificationService, + this.inlineMenuFieldQualificationService, this.themeStateService, this.totpService, () => this.generatePassword(), diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 6ef0c278dc..919cf8c0b1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -113,6 +113,7 @@ import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extensio import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; +import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service"; import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics"; import { BrowserKeyService } from "../../key-management/browser-key.service"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -167,6 +168,7 @@ const safeProviders: SafeProvider[] = [ safeProvider(DebounceNavigationService), safeProvider(DialogService), safeProvider(PopupCloseWarningService), + safeProvider(InlineMenuFieldQualificationService), safeProvider({ provide: DEFAULT_VAULT_TIMEOUT, useValue: VaultTimeoutStringType.OnRestart, diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html index 23cc692a59..c017814332 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -6,7 +6,7 @@ -
+
{{ "sendDisabledWarning" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index b173c36d25..36343d3a66 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -5,6 +5,7 @@ import { Subject } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AUTOFILL_ID } from "@bitwarden/common/autofill/constants"; import { EventType } from "@bitwarden/common/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -33,6 +34,7 @@ describe("ViewV2Component", () => { const params$ = new Subject(); const mockNavigate = jest.fn(); const collect = jest.fn().mockResolvedValue(null); + const doAutofill = jest.fn(); const mockCipher = { id: "122-333-444", @@ -41,7 +43,7 @@ describe("ViewV2Component", () => { }; const mockVaultPopupAutofillService = { - doAutofill: jest.fn(), + doAutofill, }; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -54,6 +56,7 @@ describe("ViewV2Component", () => { beforeEach(async () => { mockNavigate.mockClear(); collect.mockClear(); + doAutofill.mockClear(); await TestBed.configureTestingModule({ imports: [ViewV2Component], @@ -148,5 +151,13 @@ describe("ViewV2Component", () => { undefined, ); })); + + it('invokes `doAutofill` when action="AUTOFILL_ID"', fakeAsync(() => { + params$.next({ action: AUTOFILL_ID }); + + flush(); // Resolve all promises + + expect(doAutofill).toHaveBeenCalledOnce(); + })); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index b1cbe8bc3e..8242fd8747 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -100,7 +100,7 @@ export class ViewV2Component { switchMap(async (cipher) => { this.cipher = cipher; this.headerText = this.setHeader(cipher.type); - if (this.loadAction === AUTOFILL_ID || this.loadAction === SHOW_AUTOFILL_BUTTON) { + if (this.loadAction === AUTOFILL_ID) { await this.vaultPopupAutofillService.doAutofill(this.cipher); } diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts index effadad07f..25a7d7594a 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts @@ -5,6 +5,7 @@ import { BehaviorSubject, of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -19,6 +20,7 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutoFillOptions, AutofillService, @@ -46,6 +48,8 @@ describe("VaultPopupAutofillService", () => { const mockPasswordRepromptService = mock(); const mockCipherService = mock(); const mockMessagingService = mock(); + const mockInlineMenuFieldQualificationService = mock(); + const mockLogService = mock(); const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -53,6 +57,12 @@ describe("VaultPopupAutofillService", () => { beforeEach(() => { jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(mockCurrentTab); + jest + .spyOn(mockInlineMenuFieldQualificationService, "isFieldForCreditCardForm") + .mockReturnValue(true); + jest + .spyOn(mockInlineMenuFieldQualificationService, "isFieldForIdentityForm") + .mockReturnValue(true); mockAutofillService.collectPageDetailsFromTab$.mockReturnValue(new BehaviorSubject([])); @@ -70,6 +80,14 @@ describe("VaultPopupAutofillService", () => { provide: AccountService, useValue: accountService, }, + { + provide: InlineMenuFieldQualificationService, + useValue: mockInlineMenuFieldQualificationService, + }, + { + provide: LogService, + useValue: mockLogService, + }, ], }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts index a2e032a54f..0f76c2f2cd 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -10,10 +10,12 @@ import { startWith, Subject, switchMap, + timeout, } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -23,6 +25,7 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutofillService, PageDetail, @@ -72,11 +75,53 @@ export class VaultPopupAutofillService { if (!tab) { return of([]); } - return this.autofillService.collectPageDetailsFromTab$(tab); + return this.autofillService + .collectPageDetailsFromTab$(tab) + .pipe(timeout({ first: 1500, with: () => of([]) })); }), shareReplay({ refCount: false, bufferSize: 1 }), ); + nonLoginCipherTypesOnPage$: Observable<{ + [CipherType.Card]: boolean; + [CipherType.Identity]: boolean; + }> = this._currentPageDetails$.pipe( + map((pageDetails) => { + let pageHasCardFields = false; + let pageHasIdentityFields = false; + + try { + if (!pageDetails) { + throw Error("No page details were provided"); + } + + for (const details of pageDetails) { + for (const field of details.details.fields) { + if (!pageHasCardFields) { + pageHasCardFields = this.inlineMenuFieldQualificationService.isFieldForCreditCardForm( + field, + details.details, + ); + } + + if (!pageHasIdentityFields) { + pageHasIdentityFields = + this.inlineMenuFieldQualificationService.isFieldForIdentityForm( + field, + details.details, + ); + } + } + } + } catch (error) { + // no-op on failure; do not show extra cipher types + this.logService.warning(error.message); + } + + return { [CipherType.Card]: pageHasCardFields, [CipherType.Identity]: pageHasIdentityFields }; + }), + ); + constructor( private autofillService: AutofillService, private i18nService: I18nService, @@ -87,6 +132,8 @@ export class VaultPopupAutofillService { private messagingService: MessagingService, private route: ActivatedRoute, private accountService: AccountService, + private logService: LogService, + private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, ) { this._currentPageDetails$.subscribe(); } diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 610d48fdc6..1900d35d9d 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -15,6 +15,7 @@ import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; @@ -39,6 +40,7 @@ describe("VaultPopupItemsService", () => { const collectionService = mock(); const vaultAutofillServiceMock = mock(); const syncServiceMock = mock(); + const inlineMenuFieldQualificationServiceMock = mock(); beforeEach(() => { allCiphers = cipherFactory(10); @@ -78,6 +80,11 @@ describe("VaultPopupItemsService", () => { url: "https://example.com", } as chrome.tabs.Tab); + vaultAutofillServiceMock.nonLoginCipherTypesOnPage$ = new BehaviorSubject({ + [CipherType.Card]: true, + [CipherType.Identity]: true, + }); + mockOrg = { id: "org1", name: "Organization 1", @@ -105,6 +112,10 @@ describe("VaultPopupItemsService", () => { { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, { provide: SyncService, useValue: syncServiceMock }, + { + provide: InlineMenuFieldQualificationService, + useValue: inlineMenuFieldQualificationServiceMock, + }, ], }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 20ac3b3de9..6c3652425c 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -62,8 +62,13 @@ export class VaultPopupItemsService { private _otherAutoFillTypes$: Observable = combineLatest([ this.vaultSettingsService.showCardsCurrentTab$, this.vaultSettingsService.showIdentitiesCurrentTab$, + this.vaultPopupAutofillService.nonLoginCipherTypesOnPage$, ]).pipe( - map(([showCards, showIdentities]) => { + map(([showCardsSettingEnabled, showIdentitiesSettingEnabled, nonLoginCipherTypesOnPage]) => { + const showCards = showCardsSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Card]; + const showIdentities = + showIdentitiesSettingEnabled && nonLoginCipherTypesOnPage[CipherType.Identity]; + return [ ...(showCards ? [CipherType.Card] : []), ...(showIdentities ? [CipherType.Identity] : []), diff --git a/apps/browser/store/windows/AppxManifest.xml b/apps/browser/store/windows/AppxManifest.xml index df02ea085c..9765506a55 100644 --- a/apps/browser/store/windows/AppxManifest.xml +++ b/apps/browser/store/windows/AppxManifest.xml @@ -12,7 +12,7 @@ Bitwarden Password Manager - 8bit Solutions LLC + Bitwarden Inc Assets/icon_50.png diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore index 040b2179fa..444c9a8510 100644 --- a/apps/desktop/.gitignore +++ b/apps/desktop/.gitignore @@ -2,3 +2,4 @@ dist-safari/ *.nupkg *.env PlugIns/safari.appex/ +xcuserdata/ diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 53c20b7faf..9a8bc45ae2 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -162,7 +162,7 @@ "applicationId": "bitwardendesktop", "identityName": "8bitSolutionsLLC.bitwardendesktop", "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", - "publisherDisplayName": "8bit Solutions LLC", + "publisherDisplayName": "Bitwarden Inc", "languages": [ "en-US", "af", diff --git a/apps/desktop/macos/README.md b/apps/desktop/macos/README.md new file mode 100644 index 0000000000..6e016144b4 --- /dev/null +++ b/apps/desktop/macos/README.md @@ -0,0 +1,23 @@ +# MacOS Extensions for Desktop Apps + +This folder contains an Xcode project that builds macOS extensions for our desktop app. The extensions are used to provide additional functionality to the desktop app, such as autofill (password and passkeys). + +## Manage loaded extensions + +macOS automatically loads extensions from apps, even if they have never been used (especially if built with Xcode). This can be confusing when you have multiple copies of the same application. To see where an extension is loaded from, use the following command: + +```bash +# To list all extensions +pluginkit -m -v + +# To list a specific extension +pluginkit -m -v -i com.bitwarden.desktop.autofill-extension +``` + +To unregister an extension, you can either remove it from your filesystem, or use the following command: + +```bash +pluginkit -r +``` + +where the path to the .appex file can be found in the output of the first command. diff --git a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib new file mode 100644 index 0000000000..ace3497a58 --- /dev/null +++ b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift new file mode 100644 index 0000000000..d5c5cabeee --- /dev/null +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -0,0 +1,93 @@ +// +// CredentialProviderViewController.swift +// autofill-extension +// +// Created by Andreas Coroiu on 2023-12-21. +// + +import AuthenticationServices +import os + +class CredentialProviderViewController: ASCredentialProviderViewController { + let logger = Logger() + + /* + Implement this method if your extension supports showing credentials in the QuickType bar. + When the user selects a credential from your app, this method will be called with the + ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore. + Provide the password by completing the extension request with the associated ASPasswordCredential. + If using the credential would require showing custom UI for authenticating the user, cancel + the request with error code ASExtensionError.userInteractionRequired. + + override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { + let databaseIsUnlocked = true + if (databaseIsUnlocked) { + let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + } else { + self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) + } + } + */ + + /* + Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with + ASExtensionError.userInteractionRequired. In this case, the system may present your extension's + UI and call this method. Show appropriate UI for authenticating the user then provide the password + by completing the extension request with the associated ASPasswordCredential. + + override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { + } + */ + + @IBAction func cancel(_ sender: AnyObject?) { + self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + } + + @IBAction func passwordSelected(_ sender: AnyObject?) { + let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + } + + override func prepareInterfaceForExtensionConfiguration() { + logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called") + } + + override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { + logger.log("[autofill-extension] prepare interface for registration request \(registrationRequest.description)") + +// self.extensionContext.cancelRequest(withError: ExampleError.nope) + } + + override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) { + logger.log("[autofill-extension] prepare interface for credential request \(credentialRequest.description)") + } + + /* + Prepare your UI to list available credentials for the user to choose from. The items in + 'serviceIdentifiers' describe the service the user is logging in to, so your extension can + prioritize the most relevant credentials in the list. + */ + override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { + logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)") + + for serviceIdentifier in serviceIdentifiers { + logger.log(" service: \(serviceIdentifier.identifier)") + } + } + + override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { + logger.log("[autofill-extension] prepareInterfaceToProvideCredential for credentialIdentity: \(credentialIdentity.user)") + } + + override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) { + logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)") + + for serviceIdentifier in serviceIdentifiers { + logger.log(" service: \(serviceIdentifier.identifier)") + } + + logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)") + } + +} diff --git a/apps/desktop/macos/autofill-extension/Info.plist b/apps/desktop/macos/autofill-extension/Info.plist new file mode 100644 index 0000000000..539cfa35b9 --- /dev/null +++ b/apps/desktop/macos/autofill-extension/Info.plist @@ -0,0 +1,23 @@ + + + + + NSExtension + + NSExtensionAttributes + + ASCredentialProviderExtensionCapabilities + + ProvidesPasskeys + + + ASCredentialProviderExtensionShowsConfigurationUI + + + NSExtensionPointIdentifier + com.apple.authentication-services-credential-provider-ui + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).CredentialProviderViewController + + + diff --git a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements new file mode 100644 index 0000000000..2e600a8d52 --- /dev/null +++ b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.authentication-services.autofill-credential-provider + + com.apple.security.app-sandbox + + + diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..313b158895 --- /dev/null +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -0,0 +1,367 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; }; + E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; }; + E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; + E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; + E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProviderViewController.swift; sourceTree = ""; }; + E1DF71442B342F6900F29026 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CredentialProviderViewController.xib; sourceTree = ""; }; + E1DF71462B342F6900F29026 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E1DF71472B342F6900F29026 /* autofill_extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = autofill_extension.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E1DF71392B342F6900F29026 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E1DF711D2B342E2800F29026 = { + isa = PBXGroup; + children = ( + 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */, + E1DF71402B342F6900F29026 /* autofill-extension */, + E1DF713D2B342F6900F29026 /* Frameworks */, + E1DF71272B342E2800F29026 /* Products */, + ); + sourceTree = ""; + }; + E1DF71272B342E2800F29026 /* Products */ = { + isa = PBXGroup; + children = ( + E1DF713C2B342F6900F29026 /* autofill-extension.appex */, + ); + name = Products; + sourceTree = ""; + }; + E1DF713D2B342F6900F29026 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + E1DF71402B342F6900F29026 /* autofill-extension */ = { + isa = PBXGroup; + children = ( + E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */, + E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */, + E1DF71462B342F6900F29026 /* Info.plist */, + E1DF71472B342F6900F29026 /* autofill_extension.entitlements */, + ); + path = "autofill-extension"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E1DF713B2B342F6900F29026 /* autofill-extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = E1DF714E2B342F6900F29026 /* Build configuration list for PBXNativeTarget "autofill-extension" */; + buildPhases = ( + E1DF71382B342F6900F29026 /* Sources */, + E1DF71392B342F6900F29026 /* Frameworks */, + E1DF713A2B342F6900F29026 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "autofill-extension"; + productName = "autofill-extension"; + productReference = E1DF713C2B342F6900F29026 /* autofill-extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E1DF711E2B342E2800F29026 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + E1DF713B2B342F6900F29026 = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = E1DF71212B342E2800F29026 /* Build configuration list for PBXProject "desktop" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E1DF711D2B342E2800F29026; + productRefGroup = E1DF71272B342E2800F29026 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E1DF713B2B342F6900F29026 /* autofill-extension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E1DF713A2B342F6900F29026 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E1DF71382B342F6900F29026 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + E1DF71442B342F6900F29026 /* Base */, + ); + name = CredentialProviderViewController.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E1DF71332B342E2900F29026 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E1DF71342B342E2900F29026 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + E1DF714C2B342F6900F29026 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = LTZ2PFU5D6; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "autofill-extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Bitwarden; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + E1DF714D2B342F6900F29026 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = LTZ2PFU5D6; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "autofill-extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = Bitwarden; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E1DF71212B342E2800F29026 /* Build configuration list for PBXProject "desktop" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E1DF71332B342E2900F29026 /* Debug */, + E1DF71342B342E2900F29026 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E1DF714E2B342F6900F29026 /* Build configuration list for PBXNativeTarget "autofill-extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E1DF714C2B342F6900F29026 /* Debug */, + E1DF714D2B342F6900F29026 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E1DF711E2B342E2800F29026 /* Project object */; +} diff --git a/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/desktop/macos/production.xcconfig b/apps/desktop/macos/production.xcconfig new file mode 100644 index 0000000000..f06f2bf736 --- /dev/null +++ b/apps/desktop/macos/production.xcconfig @@ -0,0 +1,11 @@ +// +// Production.xcconfig +// desktop +// +// Created by Vince Grassia on 7/25/24. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 +CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application +PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = Bitwarden Desktop Autofill App Store 2024 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c9e33b7110..ae19e5f93c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -23,6 +23,7 @@ "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", "build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch", + "build:macos-extension": "node scripts/build-macos-extension.js", "build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js", "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch", @@ -33,11 +34,13 @@ "electron:ignore": "node ./scripts/start.js --ignore-certificate-errors", "clean:dist": "rimraf ./dist", "pack:dir": "npm run clean:dist && electron-builder --dir -p never", + "pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never", "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", "pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never", + "pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension && electron-builder --mac mas-dev --universal -p never", "pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"", "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml new file mode 100644 index 0000000000..234d37905c --- /dev/null +++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml @@ -0,0 +1,43 @@ +app-id: com.bitwarden.desktop +runtime: org.freedesktop.Platform +runtime-version: "24.08" +sdk: org.freedesktop.Sdk +base: org.electronjs.Electron2.BaseApp +base-version: "24.08" +command: bitwarden.sh +finish-args: + - --share=ipc + - --share=network + - --socket=wayland + - --socket=x11 + - --device=dri + - --env=XDG_CURRENT_DESKTOP=Unity + - --env=XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons + - --talk-name=org.kde.StatusNotifierWatcher + - --talk-name=org.freedesktop.Notifications + - --talk-name=org.freedesktop.secrets + - --talk-name=com.canonical.AppMenu.Registrar + - --system-talk-name=org.freedesktop.PolicyKit1 + # Lock on lockscreen + - --talk-name=org.gnome.ScreenSaver + - --talk-name=org.freedesktop.ScreenSaver + - --system-talk-name=org.freedesktop.login1 + - --filesystem=xdg-download +modules: + - name: bitwarden-desktop + buildsystem: simple + build-commands: + - mkdir -p /app/bin + - mkdir -p /app/bin/Bitwarden/ + - cp -r ./* /app/bin/ + - install bitwarden.sh /app/bin/bitwarden.sh + sources: + - type: dir + path: ../dist/linux-unpacked + - type: script + dest-filename: bitwarden.sh + commands: + - ulimit -c 0 + - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" + - exec zypak-wrapper /app/bin/bitwarden-app --ozone-platform-hint=auto + --enable-features=WaylandWindowDecorations "$@" diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist index 48f7bf5cec..915232b83f 100644 --- a/apps/desktop/resources/entitlements.mac.plist +++ b/apps/desktop/resources/entitlements.mac.plist @@ -8,5 +8,7 @@ com.apple.security.cs.disable-library-validation + com.apple.developer.authentication-services.autofill-credential-provider + diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist index 3ee76423e4..3634c84f81 100644 --- a/apps/desktop/resources/entitlements.mas.inherit.plist +++ b/apps/desktop/resources/entitlements.mas.inherit.plist @@ -10,5 +10,7 @@ com.apple.security.cs.disable-library-validation + com.apple.developer.authentication-services.autofill-credential-provider + diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist index d42ade962c..9ab2d3824a 100644 --- a/apps/desktop/resources/entitlements.mas.plist +++ b/apps/desktop/resources/entitlements.mas.plist @@ -16,6 +16,8 @@ com.apple.security.files.user-selected.read-write + com.apple.developer.authentication-services.autofill-credential-provider + com.apple.security.temporary-exception.files.home-relative-path.read-write /Library/Application Support/Mozilla/NativeMessagingHosts/ diff --git a/apps/desktop/scripts/after-sign.js b/apps/desktop/scripts/after-sign.js index 69c078a13b..dc60e9d183 100644 --- a/apps/desktop/scripts/after-sign.js +++ b/apps/desktop/scripts/after-sign.js @@ -15,36 +15,62 @@ async function run(context) { const appName = context.packager.appInfo.productFilename; const appPath = `${context.appOutDir}/${appName}.app`; const macBuild = context.electronPlatformName === "darwin"; - const copyPlugIn = ["darwin", "mas"].includes(context.electronPlatformName); + const copySafariExtension = ["darwin", "mas"].includes(context.electronPlatformName); + const copyAutofillExtension = ["mas"].includes(context.electronPlatformName); - if (copyPlugIn) { + let shouldResign = false; + + // cannot use extraFiles because it modifies the extensions .plist and makes it invalid + if (copyAutofillExtension) { + console.log("### Copying autofill extension"); + const extensionPath = path.join(__dirname, "../macos/dist/autofill-extension.appex"); + if (!fse.existsSync(extensionPath)) { + console.log("### Autofill extension not found - skipping"); + } else { + if (!fse.existsSync(path.join(appPath, "Contents/PlugIns"))) { + fse.mkdirSync(path.join(appPath, "Contents/PlugIns")); + } + fse.copySync(extensionPath, path.join(appPath, "Contents/PlugIns/autofill-extension.appex")); + shouldResign = true; + } + } + + if (copySafariExtension) { + console.log("### Copying safari extension"); // Copy Safari plugin to work-around https://github.com/electron-userland/electron-builder/issues/5552 const plugIn = path.join(__dirname, "../PlugIns"); - if (fse.existsSync(plugIn)) { - fse.mkdirSync(path.join(appPath, "Contents/PlugIns")); + if (!fse.existsSync(plugIn)) { + console.log("### Safari extension not found - skipping"); + } else { + if (!fse.existsSync(path.join(appPath, "Contents/PlugIns"))) { + fse.mkdirSync(path.join(appPath, "Contents/PlugIns")); + } fse.copySync( path.join(plugIn, "safari.appex"), path.join(appPath, "Contents/PlugIns/safari.appex"), ); + shouldResign = true; + } + } - // Resign to sign safari extension - if (context.electronPlatformName === "mas") { - const masBuildOptions = deepAssign( - {}, - context.packager.platformSpecificBuildOptions, - context.packager.config.mas, - ); - if (context.targets.some((e) => e.name === "mas-dev")) { - deepAssign(masBuildOptions, { - type: "development", - }); - } - if (context.packager.packagerOptions.prepackaged == null) { - await context.packager.sign(appPath, context.appOutDir, masBuildOptions, context.arch); - } - } else { - await context.packager.signApp(context, true); + if (shouldResign) { + // Resign to sign safari extension + if (context.electronPlatformName === "mas") { + const masBuildOptions = deepAssign( + {}, + context.packager.platformSpecificBuildOptions, + context.packager.config.mas, + ); + if (context.targets.some((e) => e.name === "mas-dev")) { + deepAssign(masBuildOptions, { + type: "development", + }); } + if (context.packager.packagerOptions.prepackaged == null) { + await context.packager.sign(appPath, context.appOutDir, masBuildOptions, context.arch); + } + } else { + await context.packager.signApp(context, true); } } diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js new file mode 100644 index 0000000000..3aa43fb678 --- /dev/null +++ b/apps/desktop/scripts/build-macos-extension.js @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/no-var-requires, no-console */ +const child = require("child_process"); +const { exit } = require("process"); + +const fse = require("fs-extra"); + +const paths = { + macosBuild: "./macos/build", + extensionBuild: "./macos/build/Release/autofill-extension.appex", + extensionDistDir: "./macos/dist", + extensionDist: "./macos/dist/autofill-extension.appex", + macOsProject: "./macos/desktop.xcodeproj", + macOsConfig: "./macos/production.xcconfig", +}; + +async function buildMacOs() { + if (fse.existsSync(paths.macosBuild)) { + fse.removeSync(paths.macosBuild); + } + + if (fse.existsSync(paths.extensionDistDir)) { + fse.removeSync(paths.extensionDistDir); + } + + const proc = child.spawn("xcodebuild", [ + "-project", + paths.macOsProject, + "-alltargets", + "-configuration", + "Release", + "-xcconfig", + paths.macOsConfig, + ]); + stdOutProc(proc); + await new Promise((resolve, reject) => + proc.on("close", (code) => { + if (code > 0) { + console.error("xcodebuild failed with code", code); + return reject(new Error(`xcodebuild failed with code ${code}`)); + } + console.log("xcodebuild success"); + resolve(); + }), + ); + + fse.mkdirSync(paths.extensionDistDir); + fse.copySync(paths.extensionBuild, paths.extensionDist); + // Delete the build dir, otherwise MacOS will load the extension from there instead of the Bitwarden.app bundle + fse.removeSync(paths.macosBuild); +} + +function stdOutProc(proc) { + proc.stdout.on("data", (data) => console.log(data.toString())); + proc.stderr.on("data", (data) => console.error(data.toString())); +} + +buildMacOs() + .then(() => console.log("macOS build complete")) + .catch((err) => { + console.error("macOS build failed", err); + exit(-1); + }); diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 6813a59ce4..c4ed44ab0c 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -55,10 +55,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { private _destroy = new Subject(); - protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - ); - constructor( private route: ActivatedRoute, private organizationService: OrganizationService, @@ -101,14 +97,9 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { switchMap((organization) => this.providerService.get$(organization.providerId)), ); - this.organizationIsUnmanaged$ = combineLatest([ - this.consolidatedBillingEnabled$, - this.organization$, - provider$, - ]).pipe( + this.organizationIsUnmanaged$ = combineLatest([this.organization$, provider$]).pipe( map( - ([consolidatedBillingEnabled, organization, provider]) => - !consolidatedBillingEnabled || + ([organization, provider]) => !organization.hasProvider || !provider || provider.providerStatus !== ProviderStatusType.Billable, diff --git a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts index a915d8f8a6..0f6baa5f32 100644 --- a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts +++ b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts @@ -4,22 +4,11 @@ import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; export const organizationIsUnmanaged: CanActivateFn = async (route: ActivatedRouteSnapshot) => { - const configService = inject(ConfigService); const organizationService = inject(OrganizationService); const providerService = inject(ProviderService); - const consolidatedBillingEnabled = await configService.getFeatureFlag( - FeatureFlag.EnableConsolidatedBilling, - ); - - if (!consolidatedBillingEnabled) { - return true; - } - const organization = await organizationService.get(route.params.organizationId); if (!organization.hasProvider) { 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 c9cd56849c..61c13a26e0 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 @@ -60,10 +60,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - ); - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( FeatureFlag.AC2476_DeprecateStripeSourcesAPI, ); @@ -120,8 +116,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); - const consolidatedBillingEnabled = await firstValueFrom(this.enableConsolidatedBilling$); - const isIndependentOrganizationOwner = !this.userOrg.hasProvider && this.userOrg.isOwner; const isResoldOrganizationOwner = this.userOrg.hasReseller && this.userOrg.isOwner; const isMSPUser = this.userOrg.hasProvider && this.userOrg.isProviderUser; @@ -131,7 +125,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy ); this.organizationIsManagedByConsolidatedBillingMSP = - consolidatedBillingEnabled && this.userOrg.hasProvider && metadata.isManaged; + this.userOrg.hasProvider && metadata.isManaged; this.showSubscription = isIndependentOrganizationOwner || diff --git a/apps/web/src/app/components/environment-selector/environment-selector.component.html b/apps/web/src/app/components/environment-selector/environment-selector.component.html index 4be0db2ba4..a1fb1a8a0f 100644 --- a/apps/web/src/app/components/environment-selector/environment-selector.component.html +++ b/apps/web/src/app/components/environment-selector/environment-selector.component.html @@ -17,7 +17,7 @@
- {{ "server" | i18n }}: + {{ "accessing" | i18n }}: {{ currentRegion?.domain }} diff --git a/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.spec.ts b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.spec.ts index b34730bd32..e3011604a4 100644 --- a/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.spec.ts +++ b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.spec.ts @@ -4,7 +4,11 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + MemberCipherDetailsApiService, + PasswordHealthService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -46,6 +50,14 @@ describe("PasswordHealthMembersUriComponent", () => { url: of([]), }, }, + { + provide: MemberCipherDetailsApiService, + useValue: mock(), + }, + { + provide: ApiService, + useValue: mock(), + }, ], }).compileComponents(); }); diff --git a/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.ts b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.ts index c977c82953..c8aea97ef7 100644 --- a/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.ts +++ b/apps/web/src/app/tools/risk-insights/password-health-members-uri.component.ts @@ -6,7 +6,10 @@ import { map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + MemberCipherDetailsApiService, + PasswordHealthService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -25,8 +28,6 @@ import { // eslint-disable-next-line no-restricted-imports import { HeaderModule } from "../../layouts/header/header.module"; // eslint-disable-next-line no-restricted-imports -import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; -// eslint-disable-next-line no-restricted-imports import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; @Component({ @@ -35,7 +36,6 @@ import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; templateUrl: "password-health-members-uri.component.html", imports: [ BadgeModule, - OrganizationBadgeModule, CommonModule, ContainerComponent, PipesModule, @@ -43,7 +43,7 @@ import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; HeaderModule, TableModule, ], - providers: [PasswordHealthService], + providers: [PasswordHealthService, MemberCipherDetailsApiService], }) export class PasswordHealthMembersURIComponent implements OnInit { passwordStrengthMap = new Map(); @@ -74,6 +74,7 @@ export class PasswordHealthMembersURIComponent implements OnInit { protected auditService: AuditService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, + protected memberCipherDetailsApiService: MemberCipherDetailsApiService, ) {} ngOnInit() { @@ -93,6 +94,7 @@ export class PasswordHealthMembersURIComponent implements OnInit { this.passwordStrengthService, this.auditService, this.cipherService, + this.memberCipherDetailsApiService, organizationId, ); diff --git a/apps/web/src/app/tools/risk-insights/password-health-members.component.ts b/apps/web/src/app/tools/risk-insights/password-health-members.component.ts index 2581de78ed..66ff348e9f 100644 --- a/apps/web/src/app/tools/risk-insights/password-health-members.component.ts +++ b/apps/web/src/app/tools/risk-insights/password-health-members.component.ts @@ -5,7 +5,10 @@ import { ActivatedRoute } from "@angular/router"; import { debounceTime, map } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + MemberCipherDetailsApiService, + PasswordHealthService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -18,12 +21,10 @@ import { TableModule, ToastService, } from "@bitwarden/components"; -import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "../../layouts/header/header.module"; // eslint-disable-next-line no-restricted-imports import { SharedModule } from "../../shared"; -import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; // eslint-disable-next-line no-restricted-imports import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; @@ -31,17 +32,8 @@ import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; standalone: true, selector: "tools-password-health-members", templateUrl: "password-health-members.component.html", - imports: [ - CardComponent, - OrganizationBadgeModule, - PipesModule, - HeaderModule, - SearchModule, - FormsModule, - SharedModule, - TableModule, - ], - providers: [PasswordHealthService], + imports: [PipesModule, HeaderModule, SearchModule, FormsModule, SharedModule, TableModule], + providers: [PasswordHealthService, MemberCipherDetailsApiService], }) export class PasswordHealthMembersComponent implements OnInit { passwordStrengthMap = new Map(); @@ -69,6 +61,7 @@ export class PasswordHealthMembersComponent implements OnInit { protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected toastService: ToastService, + protected memberCipherDetailsApiService: MemberCipherDetailsApiService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) @@ -92,6 +85,7 @@ export class PasswordHealthMembersComponent implements OnInit { this.passwordStrengthService, this.auditService, this.cipherService, + this.memberCipherDetailsApiService, organizationId, ); diff --git a/apps/web/src/app/tools/risk-insights/password-health.component.spec.ts b/apps/web/src/app/tools/risk-insights/password-health.component.spec.ts index 50295b435b..5e934e3edf 100644 --- a/apps/web/src/app/tools/risk-insights/password-health.component.spec.ts +++ b/apps/web/src/app/tools/risk-insights/password-health.component.spec.ts @@ -4,7 +4,11 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + MemberCipherDetailsApiService, + PasswordHealthService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -30,6 +34,8 @@ describe("PasswordHealthComponent", () => { { provide: CipherService, useValue: mock() }, { provide: I18nService, useValue: mock() }, { provide: AuditService, useValue: mock() }, + { provide: ApiService, useValue: mock() }, + { provide: MemberCipherDetailsApiService, useValue: mock() }, { provide: PasswordStrengthServiceAbstraction, useValue: mock(), diff --git a/apps/web/src/app/tools/risk-insights/password-health.component.ts b/apps/web/src/app/tools/risk-insights/password-health.component.ts index c3c1732854..058cfb86da 100644 --- a/apps/web/src/app/tools/risk-insights/password-health.component.ts +++ b/apps/web/src/app/tools/risk-insights/password-health.component.ts @@ -6,7 +6,10 @@ import { map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; // eslint-disable-next-line no-restricted-imports -import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + MemberCipherDetailsApiService, + PasswordHealthService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -41,7 +44,7 @@ import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; HeaderModule, TableModule, ], - providers: [PasswordHealthService], + providers: [PasswordHealthService, MemberCipherDetailsApiService], }) export class PasswordHealthComponent implements OnInit { passwordStrengthMap = new Map(); @@ -62,6 +65,7 @@ export class PasswordHealthComponent implements OnInit { protected auditService: AuditService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, + protected memberCipherDetailsApiService: MemberCipherDetailsApiService, ) {} ngOnInit() { @@ -81,6 +85,7 @@ export class PasswordHealthComponent implements OnInit { this.passwordStrengthService, this.auditService, this.cipherService, + this.memberCipherDetailsApiService, organizationId, ); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts index b71abe075e..872a4cdff5 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts @@ -4,74 +4,72 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; -const mockMemberCipherDetails: any = { - data: [ - { - userName: "David Brent", - email: "david.brent@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab1", - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - ], - }, - { - userName: "Tim Canterbury", - email: "tim.canterbury@wernhamhogg.uk", - usesKeyConnector: false, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - ], - }, - { - userName: "Gareth Keenan", - email: "gareth.keenan@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - "cbea34a8-bde4-46ad-9d19-b05001227nm7", - ], - }, - { - userName: "Dawn Tinsley", - email: "dawn.tinsley@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - ], - }, - { - userName: "Keith Bishop", - email: "keith.bishop@wernhamhogg.uk", - usesKeyConnector: false, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab1", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - ], - }, - { - userName: "Chris Finch", - email: "chris.finch@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - ], - }, - ], -}; +export const mockMemberCipherDetails: any = [ + { + userName: "David Brent", + email: "david.brent@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab1", + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + ], + }, + { + userName: "Tim Canterbury", + email: "tim.canterbury@wernhamhogg.uk", + usesKeyConnector: false, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + ], + }, + { + userName: "Gareth Keenan", + email: "gareth.keenan@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + "cbea34a8-bde4-46ad-9d19-b05001227nm7", + ], + }, + { + userName: "Dawn Tinsley", + email: "dawn.tinsley@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + ], + }, + { + userName: "Keith Bishop", + email: "keith.bishop@wernhamhogg.uk", + usesKeyConnector: false, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab1", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + ], + }, + { + userName: "Chris Finch", + email: "chris.finch@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + ], + }, +]; describe("Member Cipher Details API Service", () => { let memberCipherDetailsApiService: MemberCipherDetailsApiService; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.ts index 9351ac8777..b38f8712ad 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.ts @@ -1,8 +1,10 @@ +import { Injectable } from "@angular/core"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response"; +@Injectable() export class MemberCipherDetailsApiService { constructor(private apiService: ApiService) {} @@ -21,7 +23,6 @@ export class MemberCipherDetailsApiService { true, ); - const listResponse = new ListResponse(response, MemberCipherDetailsResponse); - return listResponse.data.map((r) => new MemberCipherDetailsResponse(r)); + return response; } } diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts index 692eb5afba..c0f77abeb7 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts @@ -3,15 +3,17 @@ import { TestBed } from "@angular/core/testing"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { mockCiphers } from "./ciphers.mock"; +import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; +import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec"; import { PasswordHealthService } from "./password-health.service"; describe("PasswordHealthService", () => { let service: PasswordHealthService; let cipherService: CipherService; + let memberCipherDetailsApiService: MemberCipherDetailsApiService; beforeEach(() => { TestBed.configureTestingModule({ @@ -35,7 +37,13 @@ describe("PasswordHealthService", () => { { provide: CipherService, useValue: { - getAllFromApiForOrganization: jest.fn().mockResolvedValue(CipherData), + getAllFromApiForOrganization: jest.fn().mockResolvedValue(mockCiphers), + }, + }, + { + provide: MemberCipherDetailsApiService, + useValue: { + getMemberCipherDetails: jest.fn().mockResolvedValue(mockMemberCipherDetails), }, }, { provide: "organizationId", useValue: "org1" }, @@ -44,6 +52,7 @@ describe("PasswordHealthService", () => { service = TestBed.inject(PasswordHealthService); cipherService = TestBed.inject(CipherService); + memberCipherDetailsApiService = TestBed.inject(MemberCipherDetailsApiService); }); it("should be created", () => { @@ -68,6 +77,10 @@ describe("PasswordHealthService", () => { expect(cipherService.getAllFromApiForOrganization).toHaveBeenCalledWith("org1"); }); + it("should fetch member cipher details", () => { + expect(memberCipherDetailsApiService.getMemberCipherDetails).toHaveBeenCalledWith("org1"); + }); + it("should populate reportCiphers with ciphers that have issues", () => { expect(service.reportCiphers.length).toBeGreaterThan(0); }); @@ -99,12 +112,12 @@ describe("PasswordHealthService", () => { it("should calculate total members per cipher", () => { expect(service.totalMembersMap.size).toBeGreaterThan(0); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(3); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toBe(5); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toBe(6); + expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(2); + expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toBe(4); + expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toBe(5); expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm5")).toBe(4); expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm7")).toBe(1); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228xy4")).toBe(7); + expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228xy4")).toBe(6); }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts index 1709261922..4070b23d29 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts @@ -1,9 +1,5 @@ import { Inject, Injectable } from "@angular/core"; -// eslint-disable-next-line no-restricted-imports -import { mockCiphers } from "@bitwarden/bit-common/tools/reports/risk-insights/services/ciphers.mock"; -// eslint-disable-next-line no-restricted-imports -import { mockMemberCipherDetailsResponse } from "@bitwarden/bit-common/tools/reports/risk-insights/services/member-cipher-details-response.mock"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -12,6 +8,8 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; +import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; + @Injectable() export class PasswordHealthService { reportCiphers: CipherView[] = []; @@ -30,21 +28,23 @@ export class PasswordHealthService { private passwordStrengthService: PasswordStrengthServiceAbstraction, private auditService: AuditService, private cipherService: CipherService, + private memberCipherDetailsApiService: MemberCipherDetailsApiService, @Inject("organizationId") private organizationId: string, ) {} async generateReport() { - let allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organizationId); - // TODO remove when actual user member data is available - allCiphers = mockCiphers; + const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organizationId); allCiphers.forEach(async (cipher) => { this.findWeakPassword(cipher); this.findReusedPassword(cipher); await this.findExposedPassword(cipher); }); - // TODO - fetch actual user member when data is available - mockMemberCipherDetailsResponse.data.forEach((user) => { + const memberCipherDetails = await this.memberCipherDetailsApiService.getMemberCipherDetails( + this.organizationId, + ); + + memberCipherDetails.forEach((user) => { user.cipherIds.forEach((cipherId: string) => { if (this.totalMembersMap.has(cipherId)) { this.totalMembersMap.set(cipherId, (this.totalMembersMap.get(cipherId) || 0) + 1); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 88f125a65a..e397c7ed8e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -8,11 +8,9 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { hasConsolidatedBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -46,7 +44,6 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On private apiService: ApiService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, activatedRoute: ActivatedRoute, dialogService: DialogService, i18nService: I18nService, @@ -72,9 +69,9 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( - hasConsolidatedBilling(this.configService), - map((hasConsolidatedBilling) => { - if (hasConsolidatedBilling) { + map((provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((isBillable) => { + if (isBillable) { return from( this.router.navigate(["../manage-client-organizations"], { relativeTo: this.activatedRoute, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 0536221caf..bd21e70a07 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -5,7 +5,7 @@ (); protected provider$: Observable; - protected hasConsolidatedBilling$: Observable; + protected isBillable: Observable; protected canAccessBilling$: Observable; protected showProviderClientVaultPrivacyWarningBanner$ = this.configService.getFeatureFlag$( @@ -58,12 +58,12 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), ); - this.hasConsolidatedBilling$ = this.provider$.pipe( - hasConsolidatedBilling(this.configService), + this.isBillable = this.provider$.pipe( + map((provider) => provider?.providerStatus === ProviderStatusType.Billable), takeUntil(this.destroy$), ); - this.canAccessBilling$ = combineLatest([this.hasConsolidatedBilling$, this.provider$]).pipe( + this.canAccessBilling$ = combineLatest([this.isBillable, this.provider$]).pipe( map( ([hasConsolidatedBilling, provider]) => hasConsolidatedBilling && provider.isProviderAdmin, ), diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index 482b85b712..33a20444c2 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -24,13 +24,11 @@ {{ "billingEmail" | i18n }} - {{ - "providerBillingEmailHint" | i18n - }} + {{ "providerBillingEmailHint" | i18n }}
- + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index aaad0ce457..72d954e8cd 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -1,14 +1,13 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, Subject, switchMap } from "rxjs"; +import { Subject, switchMap } from "rxjs"; import { first, takeUntil } from "rxjs/operators"; import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -34,10 +33,6 @@ export class SetupComponent implements OnInit, OnDestroy { billingEmail: ["", [Validators.required, Validators.email]], }); - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - ); - private destroy$ = new Subject(); constructor( @@ -112,13 +107,9 @@ export class SetupComponent implements OnInit, OnDestroy { submit = async () => { try { - const consolidatedBillingEnabled = await firstValueFrom(this.enableConsolidatedBilling$); - this.formGroup.markAllAsTouched(); - const formIsValid = consolidatedBillingEnabled - ? this.formGroup.valid && this.manageTaxInformationComponent.touch() - : this.formGroup.valid; + const formIsValid = this.formGroup.valid && this.manageTaxInformationComponent.touch(); if (!formIsValid) { return; @@ -133,19 +124,18 @@ export class SetupComponent implements OnInit, OnDestroy { request.token = this.token; request.key = key; - if (consolidatedBillingEnabled) { - request.taxInfo = new ExpandedTaxInfoUpdateRequest(); - const taxInformation = this.manageTaxInformationComponent.getTaxInformation(); + request.taxInfo = new ExpandedTaxInfoUpdateRequest(); + const taxInformation = this.manageTaxInformationComponent.getTaxInformation(); - request.taxInfo.country = taxInformation.country; - request.taxInfo.postalCode = taxInformation.postalCode; - if (taxInformation.includeTaxId) { - request.taxInfo.taxId = taxInformation.taxId; - request.taxInfo.line1 = taxInformation.line1; - request.taxInfo.line2 = taxInformation.line2; - request.taxInfo.city = taxInformation.city; - request.taxInfo.state = taxInformation.state; - } + request.taxInfo.country = taxInformation.country; + request.taxInfo.postalCode = taxInformation.postalCode; + + if (taxInformation.includeTaxId) { + request.taxInfo.taxId = taxInformation.taxId; + request.taxInfo.line1 = taxInformation.line1; + request.taxInfo.line2 = taxInformation.line2; + request.taxInfo.city = taxInformation.city; + request.taxInfo.state = taxInformation.state; } const provider = await this.providerApiService.postProviderSetup(this.providerId, request); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts index a870b053db..b41702e47a 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts @@ -6,13 +6,11 @@ import { switchMap } from "rxjs/operators"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; -import { hasConsolidatedBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -47,7 +45,6 @@ export class ManageClientsComponent extends BaseClientsComponent { constructor( private billingApiService: BillingApiService, - private configService: ConfigService, private providerService: ProviderService, private router: Router, activatedRoute: ActivatedRoute, @@ -73,9 +70,9 @@ export class ManageClientsComponent extends BaseClientsComponent { switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( - hasConsolidatedBilling(this.configService), - map((hasConsolidatedBilling) => { - if (!hasConsolidatedBilling) { + map((provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((isBillable) => { + if (!isBillable) { return from( this.router.navigate(["../clients"], { relativeTo: this.activatedRoute, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts b/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts index 213b9a5368..60dbf4b3b8 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts @@ -4,24 +4,13 @@ import { firstValueFrom } from "rxjs"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; export const hasConsolidatedBilling: CanActivateFn = async (route: ActivatedRouteSnapshot) => { - const configService = inject(ConfigService); const providerService = inject(ProviderService); const provider = await firstValueFrom(providerService.get$(route.params.providerId)); - const consolidatedBillingEnabled = await configService.getFeatureFlag( - FeatureFlag.EnableConsolidatedBilling, - ); - - if ( - !consolidatedBillingEnabled || - !provider || - provider.providerStatus !== ProviderStatusType.Billable - ) { + if (!provider || provider.providerStatus !== ProviderStatusType.Billable) { return createUrlTreeFromSnapshot(route, ["/providers", route.params.providerId]); } diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 952f2071e9..1ca2df6b73 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -713,7 +713,11 @@ export class AddEditComponent implements OnInit, OnDestroy { } protected deleteCipher() { - const asAdmin = this.organization?.canEditAllCiphers || !this.cipher.collectionIds; + // cipher.collectionIds may be null or an empty array. Either is a valid indication that the item is unassigned. + const asAdmin = + this.organization?.canEditAllCiphers || + !this.cipher.collectionIds || + this.cipher.collectionIds.length === 0; return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id, asAdmin) : this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 8a0ac4b718..3323b6eca0 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,18 +1,19 @@ -
- + -
+
@@ -36,14 +37,14 @@ [ngClass]="{ 'tw-max-w-md': maxWidth === 'md', 'tw-max-w-3xl': maxWidth === '3xl' }" >
-