From ca554897be62be48ce0b0a80dead40948d3a1ad5 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:39:34 -0500 Subject: [PATCH 01/15] [PM-24269] Enable ownership field for personal items (#16069) * remove global check for personal ownership as `setFormState` now handles it * ensure that the organizationId is disabled for new ciphers * only check for personal ownership change for enabling/disabling the entire form - this ensure that it is only applied when the data ownership policy is applied - The bug was caused by a regular user that wasn't in an organization, their form was getting fully disabled when it shouldn't. * fix type checking * do not disable organization id after an organization is selected --- .../item-details-section.component.spec.ts | 93 ++++++++++++++++++- .../item-details-section.component.ts | 19 ++-- 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index e3d863a0af3..c41e58f679e 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -15,6 +15,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SelectComponent } from "@bitwarden/components"; @@ -62,16 +63,22 @@ describe("ItemDetailsSectionComponent", () => { let mockPolicyService: MockProxy; const activeAccount$ = new BehaviorSubject<{ email: string }>({ email: "test@example.com" }); - const getInitialCipherView = jest.fn(() => null); + const getInitialCipherView = jest.fn(() => null); const initializedWithCachedCipher = jest.fn(() => false); + const disableFormFields = jest.fn(); + const enableFormFields = jest.fn(); beforeEach(async () => { getInitialCipherView.mockClear(); initializedWithCachedCipher.mockClear(); + disableFormFields.mockClear(); + enableFormFields.mockClear(); cipherFormProvider = mock({ getInitialCipherView, initializedWithCachedCipher, + disableFormFields, + enableFormFields, }); i18nService = mock(); i18nService.collator = { @@ -151,7 +158,7 @@ describe("ItemDetailsSectionComponent", () => { folderId: "folder1", collectionIds: ["col1"], favorite: true, - }); + } as CipherView); await component.ngOnInit(); tick(); @@ -420,7 +427,7 @@ describe("ItemDetailsSectionComponent", () => { folderId: "folder1", collectionIds: ["col1", "col2"], favorite: true, - }); + } as CipherView); component.config.organizations = [{ id: "org1" } as Organization]; component.config.collections = [ createMockCollection("col1", "Collection 1", "org1") as CollectionView, @@ -467,7 +474,7 @@ describe("ItemDetailsSectionComponent", () => { folderId: "folder1", collectionIds: ["col1", "col2", "col3"], favorite: true, - }); + } as CipherView); component.originalCipherView = { name: "cipher1", organizationId: "org1", @@ -513,6 +520,7 @@ describe("ItemDetailsSectionComponent", () => { expect(component["collectionOptions"].map((c) => c.id)).toEqual(["col1", "col2", "col3"]); }); }); + describe("readonlyCollections", () => { beforeEach(() => { component.config.mode = "edit"; @@ -594,4 +602,81 @@ describe("ItemDetailsSectionComponent", () => { expect(result).toBeUndefined(); }); }); + + describe("form status when editing a cipher", () => { + beforeEach(() => { + component.config.mode = "edit"; + component.config.originalCipher = new Cipher(); + component.originalCipherView = { + name: "cipher1", + organizationId: null, + folderId: "folder1", + collectionIds: ["col1", "col2", "col3"], + favorite: true, + } as unknown as CipherView; + }); + + describe("when personal ownership is not allowed", () => { + beforeEach(() => { + component.config.organizationDataOwnershipDisabled = false; // disallow personal ownership + component.config.organizations = [{ id: "orgId" } as Organization]; + }); + + describe("cipher does not belong to an organization", () => { + beforeEach(() => { + getInitialCipherView.mockReturnValue(component.originalCipherView!); + }); + + it("enables organizationId", async () => { + await component.ngOnInit(); + + expect(component.itemDetailsForm.controls.organizationId.disabled).toBe(false); + }); + + it("disables the rest of the form", async () => { + await component.ngOnInit(); + + expect(disableFormFields).toHaveBeenCalled(); + expect(enableFormFields).not.toHaveBeenCalled(); + }); + }); + + describe("cipher belongs to an organization", () => { + beforeEach(() => { + component.originalCipherView.organizationId = "org-id"; + getInitialCipherView.mockReturnValue(component.originalCipherView); + }); + + it("enables the rest of the form", async () => { + await component.ngOnInit(); + + expect(disableFormFields).not.toHaveBeenCalled(); + expect(enableFormFields).toHaveBeenCalled(); + }); + }); + }); + + describe("when an ownership change is not allowed", () => { + beforeEach(() => { + component.config.organizationDataOwnershipDisabled = true; // allow personal ownership + component.originalCipherView!.organizationId = undefined; + }); + + it("disables organizationId when the cipher is owned by an organization", async () => { + component.originalCipherView!.organizationId = "orgId"; + + await component.ngOnInit(); + + expect(component.itemDetailsForm.controls.organizationId.disabled).toBe(true); + }); + + it("disables organizationId when personal ownership is allowed and the user has no organizations available", async () => { + component.config.organizations = []; + + await component.ngOnInit(); + + expect(component.itemDetailsForm.controls.organizationId.disabled).toBe(true); + }); + }); + }); }); diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index bc5e7c43d12..978675e6ad9 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -225,10 +225,9 @@ export class ItemDetailsSectionComponent implements OnInit { }); await this.updateCollectionOptions(this.initialValues?.collectionIds); } + this.setFormState(); - if (!this.allowOwnershipChange) { - this.itemDetailsForm.controls.organizationId.disable(); - } + this.itemDetailsForm.controls.organizationId.valueChanges .pipe( takeUntilDestroyed(this.destroyRef), @@ -241,22 +240,28 @@ export class ItemDetailsSectionComponent implements OnInit { } /** - * When the cipher does not belong to an organization but the user's organization - * requires all ciphers to be owned by an organization, disable the entire form - * until the user selects an organization. Once the organization is set, enable the form. - * Ensure to properly set the collections control state when the form is enabled. + * Updates the global form and organizationId control states. */ private setFormState() { if (this.config.originalCipher && !this.allowPersonalOwnership) { + // When editing a cipher and the user cannot have personal ownership + // and the cipher is is not within the organization - force the user to + // move the cipher within the organization first before editing any other field if (this.itemDetailsForm.controls.organizationId.value === null) { this.cipherFormContainer.disableFormFields(); this.itemDetailsForm.controls.organizationId.enable(); this.favoriteButtonDisabled = true; } else { + // The "after" from the above: When editing a cipher and the user cannot have personal ownership + // and the organization is populated - re-enable the global form. this.cipherFormContainer.enableFormFields(); this.favoriteButtonDisabled = false; this.setCollectionControlState(); } + } else if (!this.allowOwnershipChange) { + // When the user cannot change the organization field, disable the organizationId control. + // This could be because they aren't a part of an organization + this.itemDetailsForm.controls.organizationId.disable({ emitEvent: false }); } } From a48c10283758fc792c282d595b28361e52cb207c Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 4 Sep 2025 08:52:30 -0700 Subject: [PATCH 02/15] fix(set-password-copy): [Auth/PM-25119] Update copy for flows where the user is setting and initial password (#16169) Updates the copy on flows where the user is setting an initial password. Instead of saying "New master password" and "Confirm new master password", it should say "Master password" and "Confirm master password" for these flows. --- .../input-password/input-password.component.html | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/libs/auth/src/angular/input-password/input-password.component.html b/libs/auth/src/angular/input-password/input-password.component.html index d56fe6a27fc..b6dc4141c27 100644 --- a/libs/auth/src/angular/input-password/input-password.component.html +++ b/libs/auth/src/angular/input-password/input-password.component.html @@ -33,7 +33,12 @@
- {{ "newMasterPass" | i18n }} + {{ + flow === InputPasswordFlow.SetInitialPasswordAccountRegistration || + flow === InputPasswordFlow.SetInitialPasswordAuthedUser + ? ("masterPassword" | i18n) + : ("newMasterPass" | i18n) + }} - {{ "confirmNewMasterPass" | i18n }} + {{ + flow === InputPasswordFlow.SetInitialPasswordAccountRegistration || + flow === InputPasswordFlow.SetInitialPasswordAuthedUser + ? ("confirmMasterPassword" | i18n) + : ("confirmNewMasterPass" | i18n) + }} Date: Thu, 4 Sep 2025 18:05:18 +0200 Subject: [PATCH 03/15] Remove limitation on ssh agent + collections (#15441) --- .../desktop/src/autofill/services/ssh-agent.service.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/autofill/services/ssh-agent.service.ts b/apps/desktop/src/autofill/services/ssh-agent.service.ts index 3909e76689a..d5aed7f3289 100644 --- a/apps/desktop/src/autofill/services/ssh-agent.service.ts +++ b/apps/desktop/src/autofill/services/ssh-agent.service.ts @@ -153,10 +153,7 @@ export class SshAgentService implements OnDestroy { if (isListRequest) { const sshCiphers = ciphers.filter( - (cipher) => - cipher.type === CipherType.SshKey && - !cipher.isDeleted && - cipher.organizationId == null, + (cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted, ); const keys = sshCiphers.map((cipher) => { return { @@ -266,10 +263,7 @@ export class SshAgentService implements OnDestroy { } const sshCiphers = ciphers.filter( - (cipher) => - cipher.type === CipherType.SshKey && - !cipher.isDeleted && - cipher.organizationId == null, + (cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted, ); const keys = sshCiphers.map((cipher) => { return { From 896f54696b1dd8891d90e884476768a9004d7aee Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Thu, 4 Sep 2025 09:33:39 -0700 Subject: [PATCH 04/15] [PM-24158] Add Premium Check (#16042) * [PM-24158] Add initial premium check * [PM-24158] Add premium membership dialog fix * [PM-24158] Small updates * [PM-24158] Set hasPremium to false upon initialization * [PM-24158] Partial update to settings component tests * [PM-24158] Fix billing mocked return value and add mac OS autotype test * [PM-24158] Add missing premium checks * [PM-24158] Update provider * [PM-24158] Renamed autotype resolved value * [PM-24158] Update missed resolvedAutotypeEnabled refactor * [PM-24158] Fix tests --- .../src/app/accounts/settings.component.html | 43 ++++++++++++------- .../app/accounts/settings.component.spec.ts | 33 +++++++++++++- .../src/app/accounts/settings.component.ts | 43 ++++++++++++++++--- .../src/app/services/services.module.ts | 2 + .../services/desktop-autotype.service.ts | 21 ++++++--- .../app/accounts/premium.component.html | 2 +- apps/desktop/src/locales/en/messages.json | 9 ++++ 7 files changed, 122 insertions(+), 31 deletions(-) diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 091864e59ae..4af12903a24 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -330,6 +330,33 @@ "enableBrowserIntegrationFingerprintDesc" | i18n }}
+
+
+ +
+ + {{ "important" | i18n }} + {{ "enableAutotypeDescriptionTransitionKey" | i18n }} + {{ "editShortcut" | i18n }} +
-
-
- -
- {{ "important" | i18n }} {{ "enableAutotypeDescription" | i18n }} -