From 40ab96a7ce382bf0ae1ce6f1af7bff72b684255b Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:54:00 +0200 Subject: [PATCH 01/74] Show disabled Sends within Send list view (#11555) With #10192 we introduced the new send-list-filters, and filtered out any disabled sends. These still need to be shown as the the other clients still support enabling/disabling Sends This will be removed once all clients use the same shared components. Co-authored-by: Daniel James Smith --- .../src/services/send-list-filters.service.spec.ts | 12 +----------- .../src/services/send-list-filters.service.ts | 5 ----- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts b/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts index f41dab18e6e..ef38938aba8 100644 --- a/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts +++ b/libs/tools/send/send-ui/src/services/send-list-filters.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from "@angular/core/testing"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, first } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -46,16 +46,6 @@ describe("SendListFiltersService", () => { expect(service.sendTypes.map((c) => c.value)).toEqual([SendType.File, SendType.Text]); }); - it("filters disabled sends", (done) => { - const sends = [{ disabled: true }, { disabled: false }, { disabled: true }] as SendView[]; - service.filterFunction$.pipe(first()).subscribe((filterFunction) => { - expect(filterFunction(sends)).toEqual([sends[1]]); - done(); - }); - - service.filterForm.patchValue({}); - }); - it("resets the filter form", () => { service.filterForm.patchValue({ sendType: SendType.Text }); service.resetFilterForm(); diff --git a/libs/tools/send/send-ui/src/services/send-list-filters.service.ts b/libs/tools/send/send-ui/src/services/send-list-filters.service.ts index 5b2c29329b6..b3a21a38332 100644 --- a/libs/tools/send/send-ui/src/services/send-list-filters.service.ts +++ b/libs/tools/send/send-ui/src/services/send-list-filters.service.ts @@ -44,11 +44,6 @@ export class SendListFiltersService { map( (filters) => (sends: SendView[]) => sends.filter((send) => { - // do not show disabled sends - if (send.disabled) { - return false; - } - if (filters.sendType !== null && send.type !== filters.sendType) { return false; } From 1c2cb4440b96f9ed44e676a8136be1e88d932164 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 15 Oct 2024 11:07:52 -0400 Subject: [PATCH 02/74] [PM-12345] Add cipher type settings for inline autofill menu (#11260) * add inline menu identity and card visibility settings state to autofill settings service * add inline menu identity and card visibility settings to autofill settings view component * add inline menu identity and card visibility settings to legacy autofill settings view component * do not show inline menu card and identity visibility settings if inline-menu-positioning-improvements feature flag is off * show card and identity inline menus based on their visibility settings * do not show identities in account creation username/email fields if user setting disallows it * reload local tab settings for inline menu visibility when an inline visibility setting value changes * take out tabSendMessageData call for inline menu visibility sub-settings --------- Co-authored-by: Cesar Gonzalez --- apps/browser/src/_locales/en/messages.json | 6 ++ .../abstractions/overlay.background.ts | 2 + .../autofill/background/overlay.background.ts | 16 +++++ .../content/abstractions/autofill-init.ts | 3 +- .../popup/settings/autofill-v1.component.html | 26 ++++++++ .../popup/settings/autofill-v1.component.ts | 40 +++++++++++- .../popup/settings/autofill.component.html | 32 +++++++++- .../popup/settings/autofill.component.ts | 26 ++++++++ .../autofill-overlay-content.service.spec.ts | 2 +- .../autofill-overlay-content.service.ts | 61 ++++++++++++++++--- .../services/autofill.service.spec.ts | 14 +++-- .../src/autofill/services/autofill.service.ts | 48 +++++++++++---- .../services/autofill-settings.service.ts | 44 +++++++++++++ 13 files changed, 291 insertions(+), 29 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 0a1c2bf2d1e..25386222d4e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1408,6 +1408,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index e91a58a84cf..abe7d097016 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -188,6 +188,8 @@ export type OverlayBackgroundExtensionMessageHandlers = { updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; checkIsFieldCurrentlyFilling: () => boolean; getAutofillInlineMenuVisibility: () => void; + getInlineMenuCardsVisibility: () => void; + getInlineMenuIdentitiesVisibility: () => void; openAutofillInlineMenu: () => void; closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void; diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index b45a4a25485..5b42a39ac11 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -132,6 +132,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(), + getInlineMenuCardsVisibility: () => this.getInlineMenuCardsVisibility(), + getInlineMenuIdentitiesVisibility: () => this.getInlineMenuIdentitiesVisibility(), openAutofillInlineMenu: () => this.openInlineMenu(false), closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message), checkAutofillInlineMenuFocused: ({ sender }) => this.checkInlineMenuFocused(sender), @@ -1483,6 +1485,20 @@ export class OverlayBackground implements OverlayBackgroundInterface { return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); } + /** + * Gets the inline menu's visibility setting for Cards from the settings service. + */ + private async getInlineMenuCardsVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$); + } + + /** + * Gets the inline menu's visibility setting for Identities from the settings service. + */ + private async getInlineMenuIdentitiesVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$); + } + /** * Gets the user's authentication status from the auth service. */ diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts index ba815a0f29a..529607949db 100644 --- a/apps/browser/src/autofill/content/abstractions/autofill-init.ts +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -1,4 +1,5 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { CipherType } from "@bitwarden/common/vault/enums"; import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum"; @@ -23,7 +24,7 @@ export type AutofillExtensionMessage = { data?: { direction?: "previous" | "next" | "current"; forceCloseInlineMenu?: boolean; - inlineMenuVisibility?: number; + newSettingValue?: InlineMenuVisibilitySetting; }; }; diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html index 9c7047c4cb7..ec8aeac37e9 100644 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html @@ -46,6 +46,32 @@

+
+
+
+ + +
+
+ + +
+
+
diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts index 8adee86bcf4..7879e4b343d 100644 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts @@ -8,10 +8,12 @@ import { InlineMenuVisibilitySetting, ClearClipboardDelaySetting, } from "@bitwarden/common/autofill/types"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy, UriMatchStrategySetting, } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -20,7 +22,6 @@ import { DialogService } from "@bitwarden/components"; import { BrowserApi } from "../../../platform/browser/browser-api"; import { enableAccountSwitching } from "../../../platform/flags"; -import { AutofillService } from "../../services/abstractions/autofill.service"; @Component({ selector: "app-autofill-v1", @@ -32,6 +33,10 @@ export class AutofillV1Component implements OnInit { protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; protected autoFillOverlayVisibilityOptions: any[]; protected disablePasswordManagerLink: string; + protected inlineMenuPositioningImprovementsEnabled: boolean = false; + protected showInlineMenuIdentities: boolean = true; + protected showInlineMenuCards: boolean = true; + inlineMenuIsEnabled: boolean = false; enableAutoFillOnPageLoad = false; autoFillOnPageLoadDefault = false; autoFillOnPageLoadOptions: any[]; @@ -50,7 +55,7 @@ export class AutofillV1Component implements OnInit { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private domainSettingsService: DomainSettingsService, - private autofillService: AutofillService, + private configService: ConfigService, private dialogService: DialogService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private messagingService: MessagingService, @@ -109,6 +114,20 @@ export class AutofillV1Component implements OnInit { this.autofillSettingsService.inlineMenuVisibility$, ); + this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); + + this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); + + this.showInlineMenuIdentities = + this.inlineMenuPositioningImprovementsEnabled && + (await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$)); + + this.showInlineMenuCards = + this.inlineMenuPositioningImprovementsEnabled && + (await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$)); + this.enableAutoFillOnPageLoad = await firstValueFrom( this.autofillSettingsService.autofillOnPageLoad$, ); @@ -140,9 +159,18 @@ export class AutofillV1Component implements OnInit { ); } + isInlineMenuEnabled() { + return ( + this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnFieldFocus || + this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick + ); + } + async updateAutoFillOverlayVisibility() { await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); await this.requestPrivacyPermission(); + + this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); } async updateAutoFillOnPageLoad() { @@ -298,4 +326,12 @@ export class AutofillV1Component implements OnInit { async updateShowIdentitiesCurrentTab() { await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); } + + async updateShowInlineMenuCards() { + await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards); + } + + async updateShowInlineMenuIdentities() { + await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities); + } } diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 99ac51bcdbc..b9e4ad222d2 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -27,7 +27,37 @@

{{ "autofillSuggestionsSectionTitle" | i18n }}

{{ "showInlineMenuOnFormFieldsDescAlt" | i18n }} - + + + + {{ "showInlineMenuIdentitiesLabel" | i18n }} + + + + + + {{ "showInlineMenuCardsLabel" | i18n }} + + + { it("updates the inlineMenuVisibility property", () => { sendMockExtensionMessage({ command: "updateAutofillInlineMenuVisibility", - data: { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, + data: { newSettingValue: AutofillOverlayVisibility.OnButtonClick }, }); expect(autofillOverlayContentService["inlineMenuVisibility"]).toEqual( diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 2e85fa22819..66b603188ec 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -9,6 +9,7 @@ import { AUTOFILL_OVERLAY_HANDLE_REPOSITION, AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT, } from "@bitwarden/common/autofill/constants"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { CipherType } from "@bitwarden/common/vault/enums"; import { @@ -51,7 +52,9 @@ import { AutoFillConstants } from "./autofill-constants"; export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { pageDetailsUpdateRequired = false; - inlineMenuVisibility: number; + inlineMenuVisibility: InlineMenuVisibilitySetting; + private showInlineMenuIdentities: boolean; + private showInlineMenuCards: boolean; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; private formFieldElements: Map, AutofillField> = new Map(); @@ -183,6 +186,18 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, ) { + if (!this.inlineMenuVisibility) { + await this.getInlineMenuVisibility(); + } + + if (this.showInlineMenuCards == null) { + await this.getInlineMenuCardsVisibility(); + } + + if (this.showInlineMenuIdentities == null) { + await this.getInlineMenuIdentitiesVisibility(); + } + if ( this.formFieldElements.has(formFieldElement) || this.isIgnoredField(autofillFieldData, pageDetails) @@ -1019,10 +1034,16 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ const { width, height, top, left } = await this.getMostRecentlyFocusedFieldRects(formFieldElement); const autofillFieldData = this.formFieldElements.get(formFieldElement); + let accountCreationFieldType = null; + if ( + // user setting allows display of identities in inline menu + this.showInlineMenuIdentities && + // `showInlineMenuAccountCreation` has been set or field is filled by Login cipher (autofillFieldData?.showInlineMenuAccountCreation || autofillFieldData?.filledByCipherType === CipherType.Login) && + // field is a username field, which is relevant to both Identity and Login ciphers this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData) ) { accountCreationFieldType = this.inlineMenuFieldQualificationService.isEmailField( @@ -1125,6 +1146,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( + this.showInlineMenuCards && this.inlineMenuFieldQualificationService.isFieldForCreditCardForm( autofillFieldData, pageDetails, @@ -1135,6 +1157,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( + this.showInlineMenuIdentities && this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm( autofillFieldData, pageDetails, @@ -1146,6 +1169,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( + this.showInlineMenuIdentities && this.inlineMenuFieldQualificationService.isFieldForIdentityForm( autofillFieldData, pageDetails, @@ -1244,6 +1268,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled"); autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled"); autofillFieldData.viewable = true; + void this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData); } @@ -1266,10 +1291,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ await this.updateMostRecentlyFocusedField(formFieldElement); } - if (!this.inlineMenuVisibility) { - await this.getInlineMenuVisibility(); - } - this.setupFormFieldElementEventListeners(formFieldElement); this.setupFormSubmissionEventListeners(formFieldElement, autofillFieldData); @@ -1291,6 +1312,30 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; } + /** + * Queries the background script for the autofill inline menu's Cards visibility setting. + * If the setting is not found, a default value of true will be used + * @private + */ + private async getInlineMenuCardsVisibility() { + const inlineMenuCardsVisibility = await this.sendExtensionMessage( + "getInlineMenuCardsVisibility", + ); + this.showInlineMenuCards = inlineMenuCardsVisibility ?? true; + } + + /** + * Queries the background script for the autofill inline menu's Identities visibility setting. + * If the setting is not found, a default value of true will be used + * @private + */ + private async getInlineMenuIdentitiesVisibility() { + const inlineMenuIdentitiesVisibility = await this.sendExtensionMessage( + "getInlineMenuIdentitiesVisibility", + ); + this.showInlineMenuIdentities = inlineMenuIdentitiesVisibility ?? true; + } + /** * Returns a value that indicates if we should hide the inline menu list due to a filled field. * @@ -1318,8 +1363,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * @param data - The data object from the extension message. */ private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) { - if (!isNaN(data?.inlineMenuVisibility)) { - this.inlineMenuVisibility = data.inlineMenuVisibility; + const newSettingValue = data?.newSettingValue; + + if (!isNaN(newSettingValue)) { + this.inlineMenuVisibility = newSettingValue; } } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 7bd08caaf33..3f33caccc41 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -75,6 +75,8 @@ describe("AutofillService", () => { let autofillService: AutofillService; const cipherService = mock(); let inlineMenuVisibilityMock$!: BehaviorSubject; + let showInlineMenuCardsMock$!: BehaviorSubject; + let showInlineMenuIdentitiesMock$!: BehaviorSubject; let autofillSettingsService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -98,8 +100,12 @@ describe("AutofillService", () => { beforeEach(() => { scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); + showInlineMenuCardsMock$ = new BehaviorSubject(false); + showInlineMenuIdentitiesMock$ = new BehaviorSubject(false); autofillSettingsService = mock(); autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; + autofillSettingsService.showInlineMenuCards$ = showInlineMenuCardsMock$; + autofillSettingsService.showInlineMenuIdentities$ = showInlineMenuIdentitiesMock$; activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; @@ -291,12 +297,12 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, + { newSettingValue: AutofillOverlayVisibility.OnButtonClick }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, + { newSettingValue: AutofillOverlayVisibility.OnButtonClick }, ); }); @@ -308,12 +314,12 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, + { newSettingValue: AutofillOverlayVisibility.OnFieldFocus }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, "updateAutofillInlineMenuVisibility", - { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, + { newSettingValue: AutofillOverlayVisibility.OnFieldFocus }, ); }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index ea68b80e84f..0b25426728e 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -130,10 +130,23 @@ export default class AutofillService implements AutofillServiceInterface { async loadAutofillScriptsOnInstall() { BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection); void this.injectAutofillScriptsInAllTabs(); + this.autofillSettingsService.inlineMenuVisibility$ .pipe(startWith(undefined), pairwise()) .subscribe(([previousSetting, currentSetting]) => - this.handleInlineMenuVisibilityChange(previousSetting, currentSetting), + this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting), + ); + + this.autofillSettingsService.showInlineMenuCards$ + .pipe(startWith(undefined), pairwise()) + .subscribe(([previousSetting, currentSetting]) => + this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting), + ); + + this.autofillSettingsService.showInlineMenuIdentities$ + .pipe(startWith(undefined), pairwise()) + .subscribe(([previousSetting, currentSetting]) => + this.handleInlineMenuVisibilitySettingsChange(previousSetting, currentSetting), ); } @@ -3043,27 +3056,36 @@ export default class AutofillService implements AutofillServiceInterface { } /** - * Updates the autofill inline menu visibility setting in all active tabs - * when the InlineMenuVisibilitySetting observable is updated. + * Updates the autofill inline menu visibility settings in all active tabs + * when the inlineMenuVisibility, showInlineMenuCards, or showInlineMenuIdentities + * observables are updated. * - * @param previousSetting - The previous setting value - * @param currentSetting - The current setting value + * @param oldSettingValue - The previous setting value + * @param newSettingValue - The current setting value + * @param cipherType - The cipher type of the changed inline menu setting */ - private async handleInlineMenuVisibilityChange( - previousSetting: InlineMenuVisibilitySetting, - currentSetting: InlineMenuVisibilitySetting, + private async handleInlineMenuVisibilitySettingsChange( + oldSettingValue: InlineMenuVisibilitySetting | boolean, + newSettingValue: InlineMenuVisibilitySetting | boolean, ) { - if (previousSetting === undefined || previousSetting === currentSetting) { + if (oldSettingValue === undefined || oldSettingValue === newSettingValue) { return; } - const inlineMenuPreviouslyDisabled = previousSetting === AutofillOverlayVisibility.Off; - const inlineMenuCurrentlyDisabled = currentSetting === AutofillOverlayVisibility.Off; - if (!inlineMenuPreviouslyDisabled && !inlineMenuCurrentlyDisabled) { + const isInlineMenuVisibilitySubSetting = + typeof oldSettingValue === "boolean" || typeof newSettingValue === "boolean"; + const inlineMenuPreviouslyDisabled = oldSettingValue === AutofillOverlayVisibility.Off; + const inlineMenuCurrentlyDisabled = newSettingValue === AutofillOverlayVisibility.Off; + + if ( + !isInlineMenuVisibilitySubSetting && + !inlineMenuPreviouslyDisabled && + !inlineMenuCurrentlyDisabled + ) { const tabs = await BrowserApi.tabsQuery({}); tabs.forEach((tab) => BrowserApi.tabSendMessageData(tab, "updateAutofillInlineMenuVisibility", { - inlineMenuVisibility: currentSetting, + newSettingValue, }), ); return; diff --git a/libs/common/src/autofill/services/autofill-settings.service.ts b/libs/common/src/autofill/services/autofill-settings.service.ts index 123f69550c3..09fdde8997b 100644 --- a/libs/common/src/autofill/services/autofill-settings.service.ts +++ b/libs/common/src/autofill/services/autofill-settings.service.ts @@ -59,6 +59,24 @@ const INLINE_MENU_VISIBILITY = new KeyDefinition( }, ); +const SHOW_INLINE_MENU_IDENTITIES = new UserKeyDefinition( + AUTOFILL_SETTINGS_DISK, + "showInlineMenuIdentities", + { + deserializer: (value: boolean) => value ?? true, + clearOn: [], + }, +); + +const SHOW_INLINE_MENU_CARDS = new UserKeyDefinition( + AUTOFILL_SETTINGS_DISK, + "showInlineMenuCards", + { + deserializer: (value: boolean) => value ?? true, + clearOn: [], + }, +); + const ENABLE_CONTEXT_MENU = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "enableContextMenu", { deserializer: (value: boolean) => value ?? true, }); @@ -86,6 +104,10 @@ export abstract class AutofillSettingsServiceAbstraction { setAutoCopyTotp: (newValue: boolean) => Promise; inlineMenuVisibility$: Observable; setInlineMenuVisibility: (newValue: InlineMenuVisibilitySetting) => Promise; + showInlineMenuIdentities$: Observable; + setShowInlineMenuIdentities: (newValue: boolean) => Promise; + showInlineMenuCards$: Observable; + setShowInlineMenuCards: (newValue: boolean) => Promise; enableContextMenu$: Observable; setEnableContextMenu: (newValue: boolean) => Promise; clearClipboardDelay$: Observable; @@ -113,6 +135,12 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti private inlineMenuVisibilityState: GlobalState; readonly inlineMenuVisibility$: Observable; + private showInlineMenuIdentitiesState: ActiveUserState; + readonly showInlineMenuIdentities$: Observable; + + private showInlineMenuCardsState: ActiveUserState; + readonly showInlineMenuCards$: Observable; + private enableContextMenuState: GlobalState; readonly enableContextMenu$: Observable; @@ -157,6 +185,14 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti map((x) => x ?? AutofillOverlayVisibility.Off), ); + this.showInlineMenuIdentitiesState = this.stateProvider.getActive(SHOW_INLINE_MENU_IDENTITIES); + this.showInlineMenuIdentities$ = this.showInlineMenuIdentitiesState.state$.pipe( + map((x) => x ?? true), + ); + + this.showInlineMenuCardsState = this.stateProvider.getActive(SHOW_INLINE_MENU_CARDS); + this.showInlineMenuCards$ = this.showInlineMenuCardsState.state$.pipe(map((x) => x ?? true)); + this.enableContextMenuState = this.stateProvider.getGlobal(ENABLE_CONTEXT_MENU); this.enableContextMenu$ = this.enableContextMenuState.state$.pipe(map((x) => x ?? true)); @@ -190,6 +226,14 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti await this.inlineMenuVisibilityState.update(() => newValue); } + async setShowInlineMenuIdentities(newValue: boolean): Promise { + await this.showInlineMenuIdentitiesState.update(() => newValue); + } + + async setShowInlineMenuCards(newValue: boolean): Promise { + await this.showInlineMenuCardsState.update(() => newValue); + } + async setEnableContextMenu(newValue: boolean): Promise { await this.enableContextMenuState.update(() => newValue); } From 15e5a6d747c508dc67fe7764f487409d61de55ae Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:16:00 -0500 Subject: [PATCH 03/74] update heading level on password generator (#11556) --- libs/tools/generator/components/src/generator.module.ts | 2 ++ .../generator/components/src/password-settings.component.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/tools/generator/components/src/generator.module.ts b/libs/tools/generator/components/src/generator.module.ts index c7dfc60bab2..96622774a3f 100644 --- a/libs/tools/generator/components/src/generator.module.ts +++ b/libs/tools/generator/components/src/generator.module.ts @@ -20,6 +20,7 @@ import { SectionHeaderComponent, SelectModule, ToggleGroupModule, + TypographyModule, } from "@bitwarden/components"; import { createRandomizer, @@ -55,6 +56,7 @@ const RANDOMIZER = new SafeInjectionToken("Randomizer"); SectionHeaderComponent, SelectModule, ToggleGroupModule, + TypographyModule, ], providers: [ safeProvider({ diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index be443784da0..fcafc789049 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -1,6 +1,6 @@ -
{{ "options" | i18n }}
+

{{ "options" | i18n }}

From e5ca6fd46030430fbc336ebc043c137affca9f0c Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:06:36 -0500 Subject: [PATCH 04/74] wrap generate & copy buttons in a flex container, flex containers do not wrap by default (#11558) --- .../components/src/credential-generator.component.html | 2 +- .../generator/components/src/password-generator.component.html | 2 +- .../generator/components/src/username-generator.component.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index 91a7c12210d..b174349ecef 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -15,7 +15,7 @@
-
+
diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index 7ec3a565dd3..9a33aa143ec 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -13,7 +13,7 @@
-
+
diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index a44637d78e5..6425cb7a38f 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -2,7 +2,7 @@
-
+
From 55ee33206f2f0eb422499fe2f542b784caa35131 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 15 Oct 2024 18:21:08 +0200 Subject: [PATCH 05/74] Fix log service not binding this (#11551) --- apps/browser/src/background/main.background.ts | 2 +- apps/browser/src/popup/app.component.ts | 2 +- apps/cli/src/service-container/service-container.ts | 2 +- apps/desktop/src/app/app.component.ts | 2 +- apps/web/src/app/app.component.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 318b856b324..819bb2a59f9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1340,7 +1340,7 @@ export default class MainBackground { } if (!supported) { - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 113cd736c6a..3c8752f3a76 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -81,7 +81,7 @@ export class AppComponent implements OnInit, OnDestroy { .subscribe((supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 7643683221a..8f8f1fa4563 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -858,7 +858,7 @@ export class ServiceContainer { } if (!supported) { - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } } } diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index d3b39218b52..dceda128c85 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -167,7 +167,7 @@ export class AppComponent implements OnInit, OnDestroy { .subscribe((supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index e6cd30caee9..7cefdd2165d 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -105,7 +105,7 @@ export class AppComponent implements OnDestroy, OnInit { .subscribe((supported) => { if (!supported) { this.logService.debug("SDK is not supported"); - this.sdkService.failedToInitialize().catch(this.logService.error); + this.sdkService.failedToInitialize().catch((e) => this.logService.error(e)); } else { this.logService.debug("SDK is supported"); } From 32d12b3d6a12f564a7a305a41069995bd4199af3 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Tue, 15 Oct 2024 11:34:14 -0500 Subject: [PATCH 06/74] [PM-7980] Inline autofill menu is not shown inside dialog html tag (#11474) * [PM-7980] Fix inline menu not showing inside dialog html tag * [PM-7980] Fix inline menu not showing inside dialog html tag * [PM-7980] Fixing an issue where a dialog element could potentially not represent itself in the #top-layer --- .../autofill/background/overlay.background.ts | 2 +- ...tofill-inline-menu-content.service.spec.ts | 56 +++++++++++------ .../autofill-inline-menu-content.service.ts | 60 +++++++++++-------- .../autofill-overlay-content.service.ts | 37 +++++++++--- libs/common/src/autofill/constants/index.ts | 2 + 5 files changed, 104 insertions(+), 53 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 5b42a39ac11..49788d67404 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -367,7 +367,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { } } - if (!this.cardAndIdentityCiphers.size) { + if (!this.cardAndIdentityCiphers?.size) { this.cardAndIdentityCiphers = null; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts index 8d5e08fc08e..c9d86cffc5c 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts @@ -14,7 +14,7 @@ describe("AutofillInlineMenuContentService", () => { let autofillInlineMenuContentService: AutofillInlineMenuContentService; let autofillInit: AutofillInit; let sendExtensionMessageSpy: jest.SpyInstance; - let observeBodyMutationsSpy: jest.SpyInstance; + let observeContainerMutationsSpy: jest.SpyInstance; const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdleCallback(resolve)); @@ -25,8 +25,8 @@ describe("AutofillInlineMenuContentService", () => { autofillInlineMenuContentService = new AutofillInlineMenuContentService(); autofillInit = new AutofillInit(domQueryService, null, autofillInlineMenuContentService); autofillInit.init(); - observeBodyMutationsSpy = jest.spyOn( - autofillInlineMenuContentService["bodyElementMutationObserver"] as any, + observeContainerMutationsSpy = jest.spyOn( + autofillInlineMenuContentService["containerElementMutationObserver"] as any, "observe", ); sendExtensionMessageSpy = jest.spyOn( @@ -51,7 +51,7 @@ describe("AutofillInlineMenuContentService", () => { describe("extension message handlers", () => { describe("closeAutofillInlineMenu message handler", () => { beforeEach(() => { - observeBodyMutationsSpy.mockImplementation(); + observeContainerMutationsSpy.mockImplementation(); }); it("closes the inline menu button", async () => { @@ -87,9 +87,9 @@ describe("AutofillInlineMenuContentService", () => { }); it("closes both inline menu elements and removes the body element mutation observer", async () => { - const unobserveBodyElementSpy = jest.spyOn( + const unobserveContainerElementSpy = jest.spyOn( autofillInlineMenuContentService as any, - "unobserveBodyElement", + "unobserveContainerElement", ); sendMockExtensionMessage({ command: "appendAutofillInlineMenuToDom", @@ -104,7 +104,7 @@ describe("AutofillInlineMenuContentService", () => { command: "closeAutofillInlineMenu", }); - expect(unobserveBodyElementSpy).toHaveBeenCalled(); + expect(unobserveContainerElementSpy).toHaveBeenCalled(); expect(sendExtensionMessageSpy).toHaveBeenCalledWith("autofillOverlayElementClosed", { overlayElement: AutofillOverlayElement.Button, }); @@ -127,7 +127,7 @@ describe("AutofillInlineMenuContentService", () => { .spyOn(autofillInlineMenuContentService as any, "isInlineMenuListVisible") .mockResolvedValue(true); jest.spyOn(globalThis.document.body, "appendChild"); - observeBodyMutationsSpy.mockImplementation(); + observeContainerMutationsSpy.mockImplementation(); }); describe("creating the inline menu button", () => { @@ -279,7 +279,8 @@ describe("AutofillInlineMenuContentService", () => { }); }); - describe("handleBodyElementMutationObserverUpdate", () => { + describe("handleContainerElementMutationObserverUpdate", () => { + let mockMutationRecord: MockProxy; let buttonElement: HTMLElement; let listElement: HTMLElement; let isInlineMenuListVisibleSpy: jest.SpyInstance; @@ -289,6 +290,7 @@ describe("AutofillInlineMenuContentService", () => {
`; + mockMutationRecord = mock({ target: globalThis.document.body } as any); buttonElement = document.querySelector(".overlay-button") as HTMLElement; listElement = document.querySelector(".overlay-list") as HTMLElement; autofillInlineMenuContentService["buttonElement"] = buttonElement; @@ -309,7 +311,9 @@ describe("AutofillInlineMenuContentService", () => { autofillInlineMenuContentService["buttonElement"] = undefined; autofillInlineMenuContentService["listElement"] = undefined; - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -323,7 +327,9 @@ describe("AutofillInlineMenuContentService", () => { ) .mockReturnValue(true); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -332,14 +338,18 @@ describe("AutofillInlineMenuContentService", () => { it("skips re-arranging the DOM elements if the last child of the body is non-existent", async () => { document.body.innerHTML = ""; - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); }); it("skips re-arranging the DOM elements if the last child of the body is the overlay list and the second to last child of the body is the overlay button", async () => { - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -349,7 +359,9 @@ describe("AutofillInlineMenuContentService", () => { listElement.remove(); isInlineMenuListVisibleSpy.mockResolvedValue(false); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); @@ -359,7 +371,9 @@ describe("AutofillInlineMenuContentService", () => { const injectedElement = document.createElement("div"); document.body.insertBefore(injectedElement, listElement); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( @@ -371,7 +385,9 @@ describe("AutofillInlineMenuContentService", () => { it("positions the overlay button before the overlay list if the elements have inserted in incorrect order", async () => { document.body.appendChild(buttonElement); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( @@ -384,7 +400,9 @@ describe("AutofillInlineMenuContentService", () => { const injectedElement = document.createElement("div"); document.body.appendChild(injectedElement); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(globalThis.document.body.insertBefore).toHaveBeenCalledWith( @@ -409,7 +427,9 @@ describe("AutofillInlineMenuContentService", () => { 1000, ); - await autofillInlineMenuContentService["handleBodyElementMutationObserverUpdate"](); + autofillInlineMenuContentService["handleContainerElementMutationObserverUpdate"]([ + mockMutationRecord, + ]); await waitForIdleCallback(); expect(persistentLastChild.style.getPropertyValue("z-index")).toBe("2147483646"); diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index 02d3ae052cc..110c1be7db8 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -30,7 +30,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte private buttonElement: HTMLElement; private listElement: HTMLElement; private inlineMenuElementsMutationObserver: MutationObserver; - private bodyElementMutationObserver: MutationObserver; + private containerElementMutationObserver: MutationObserver; private mutationObserverIterations = 0; private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout; @@ -102,7 +102,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte return; } - this.unobserveBodyElement(); + this.unobserveContainerElement(); this.closeInlineMenuButton(); this.closeInlineMenuList(); }; @@ -153,7 +153,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } if (!(await this.isInlineMenuButtonVisible())) { - this.appendInlineMenuElementToBody(this.buttonElement); + this.appendInlineMenuElementToDom(this.buttonElement); this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.Button, true); } } @@ -168,7 +168,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } if (!(await this.isInlineMenuListVisible())) { - this.appendInlineMenuElementToBody(this.listElement); + this.appendInlineMenuElementToDom(this.listElement); this.updateInlineMenuElementIsVisibleStatus(AutofillOverlayElement.List, true); } } @@ -196,8 +196,15 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * * @param element - The inline menu element to append to the body element. */ - private appendInlineMenuElementToBody(element: HTMLElement) { - this.observeBodyElement(); + private appendInlineMenuElementToDom(element: HTMLElement) { + const parentDialogElement = globalThis.document.activeElement?.closest("dialog"); + if (parentDialogElement && parentDialogElement.open && parentDialogElement.matches(":modal")) { + this.observeContainerElement(parentDialogElement); + parentDialogElement.appendChild(element); + return; + } + + this.observeContainerElement(globalThis.document.body); globalThis.document.body.appendChild(element); } @@ -276,8 +283,8 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.handleInlineMenuElementMutationObserverUpdate, ); - this.bodyElementMutationObserver = new MutationObserver( - this.handleBodyElementMutationObserverUpdate, + this.containerElementMutationObserver = new MutationObserver( + this.handleContainerElementMutationObserverUpdate, ); }; @@ -306,19 +313,17 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Sets up a mutation observer for the body element. The mutation observer is used - * to ensure that the inline menu elements are always present at the bottom of the - * body element. + * Sets up a mutation observer for the element which contains the inline menu. */ - private observeBodyElement() { - this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true }); + private observeContainerElement(element: HTMLElement) { + this.containerElementMutationObserver?.observe(element, { childList: true }); } /** - * Disconnects the mutation observer for the body element. + * Disconnects the mutation observer for the element which contains the inline menu. */ - private unobserveBodyElement() { - this.bodyElementMutationObserver?.disconnect(); + private unobserveContainerElement() { + this.containerElementMutationObserver?.disconnect(); } /** @@ -370,11 +375,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Handles the mutation observer update for the body element. This method will - * ensure that the inline menu elements are always present at the bottom of the - * body element. + * Handles the mutation observer update for the element that contains the inline menu. + * This method will ensure that the inline menu elements are always present at the + * bottom of the container. */ - private handleBodyElementMutationObserverUpdate = () => { + private handleContainerElementMutationObserverUpdate = (mutations: MutationRecord[]) => { if ( (!this.buttonElement && !this.listElement) || this.isTriggeringExcessiveMutationObserverIterations() @@ -382,15 +387,18 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte return; } - requestIdleCallbackPolyfill(this.processBodyElementMutation, { timeout: 500 }); + const containerElement = mutations[0].target as HTMLElement; + requestIdleCallbackPolyfill(() => this.processContainerElementMutation(containerElement), { + timeout: 500, + }); }; /** - * Processes the mutation of the body element. Will trigger when an + * Processes the mutation of the element that contains the inline menu. Will trigger when an * idle moment in the execution of the main thread is detected. */ - private processBodyElementMutation = async () => { - const lastChild = globalThis.document.body.lastElementChild; + private processContainerElementMutation = async (containerElement: HTMLElement) => { + const lastChild = containerElement.lastElementChild; const secondToLastChild = lastChild?.previousElementSibling; const lastChildIsInlineMenuList = lastChild === this.listElement; const lastChildIsInlineMenuButton = lastChild === this.buttonElement; @@ -424,11 +432,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte (lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) || (lastChildIsInlineMenuButton && isInlineMenuListVisible) ) { - globalThis.document.body.insertBefore(this.buttonElement, this.listElement); + containerElement.insertBefore(this.buttonElement, this.listElement); return; } - globalThis.document.body.insertBefore(lastChild, this.buttonElement); + containerElement.insertBefore(lastChild, this.buttonElement); }; /** diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 66b603188ec..1f0a38ad806 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -8,6 +8,7 @@ import { AutofillOverlayVisibility, AUTOFILL_OVERLAY_HANDLE_REPOSITION, AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT, + AUTOFILL_OVERLAY_HANDLE_SCROLL, } from "@bitwarden/common/autofill/constants"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -1647,15 +1648,28 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * the overlay elements on scroll or resize. */ private setOverlayRepositionEventListeners() { - const handler = this.useEventHandlersMemo( + const repositionHandler = this.useEventHandlersMemo( throttle(this.handleOverlayRepositionEvent, 250), AUTOFILL_OVERLAY_HANDLE_REPOSITION, ); - globalThis.addEventListener(EVENTS.SCROLL, handler, { + + const eventTargetDoesNotContainFocusedField = (element: Element) => + typeof element?.contains === "function" && !element.contains(this.mostRecentlyFocusedField); + const scrollHandler = this.useEventHandlersMemo( + throttle((event) => { + if (eventTargetDoesNotContainFocusedField(event.target as Element)) { + return; + } + repositionHandler(event); + }, 50), + AUTOFILL_OVERLAY_HANDLE_SCROLL, + ); + + globalThis.addEventListener(EVENTS.SCROLL, scrollHandler, { capture: true, passive: true, }); - globalThis.addEventListener(EVENTS.RESIZE, handler); + globalThis.addEventListener(EVENTS.RESIZE, repositionHandler); } /** @@ -1663,12 +1677,19 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ * the overlay elements on scroll or resize. */ private removeOverlayRepositionEventListeners() { - const handler = this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; - globalThis.removeEventListener(EVENTS.SCROLL, handler, { - capture: true, - }); - globalThis.removeEventListener(EVENTS.RESIZE, handler); + globalThis.removeEventListener( + EVENTS.SCROLL, + this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_SCROLL], + { + capture: true, + }, + ); + globalThis.removeEventListener( + EVENTS.RESIZE, + this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION], + ); + delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_SCROLL]; delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; } diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index 4ccec81a447..9333fa23368 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -58,6 +58,8 @@ export const NOTIFICATION_BAR_LIFESPAN_MS = 150000; // 150 seconds export const AUTOFILL_OVERLAY_HANDLE_REPOSITION = "autofill-overlay-handle-reposition-event"; +export const AUTOFILL_OVERLAY_HANDLE_SCROLL = "autofill-overlay-handle-scroll-event"; + export const UPDATE_PASSKEYS_HEADINGS_ON_SCROLL = "update-passkeys-headings-on-scroll"; export const AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT = "autofill-trigger-form-field-submit"; From c459253beb303a8a1eb896656c12ebaacba76e2f Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:27:30 +0000 Subject: [PATCH 07/74] Bumped client version(s) (#11564) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- package-lock.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 6c41f6267cc..b1a98084358 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.10.1", + "version": "2024.10.2", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 850c5c4727a..4a43e39d0a6 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.1", + "version": "2024.10.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 0b89a36d700..a5268a2bd96 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.1", + "version": "2024.10.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/package-lock.json b/package-lock.json index f16c0c43947..359c4b9275e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -194,7 +194,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.10.1" + "version": "2024.10.2" }, "apps/cli": { "name": "@bitwarden/cli", From 1eaa80de26372db5019ec79c66d44e45adeb7e94 Mon Sep 17 00:00:00 2001 From: Merissa Weinstein Date: Tue, 15 Oct 2024 13:38:13 -0500 Subject: [PATCH 08/74] Revert "Bumped client version(s) (#11564)" (#11566) This reverts commit c459253beb303a8a1eb896656c12ebaacba76e2f. --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- package-lock.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index b1a98084358..6c41f6267cc 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.10.2", + "version": "2024.10.1", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 4a43e39d0a6..850c5c4727a 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.2", + "version": "2024.10.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index a5268a2bd96..0b89a36d700 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.2", + "version": "2024.10.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/package-lock.json b/package-lock.json index 359c4b9275e..f16c0c43947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -194,7 +194,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.10.2" + "version": "2024.10.1" }, "apps/cli": { "name": "@bitwarden/cli", From 87545e4d21d699b38ff5bf8b9c7963ecf9539b32 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:48:51 -0700 Subject: [PATCH 09/74] fix logic for displaying hide email in send options (#11561) --- .../components/options/send-options.component.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index cc1400f0a6c..bcda8b57107 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -43,8 +43,13 @@

{{ "additionalOptions" | i18n }}

> {{ "sendPasswordDescV2" | i18n }} - - + + {{ "hideYourEmail" | i18n }} From 70d83feb38f69f975872dda6c83dec1d407cbff1 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:22:10 -0500 Subject: [PATCH 10/74] add custom field copy to web (#11567) --- apps/web/src/locales/en/messages.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5c876e0a8a4..96e987a7f64 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9377,6 +9377,21 @@ "editAccess": { "message": "Edit access" }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, "uppercaseDescription": { "message": "Include uppercase characters", "description": "Tooltip for the password generator uppercase character checkbox" From 178a418850dc7bd170ccfcfa6d923116cfd1052c Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:46:03 +1000 Subject: [PATCH 11/74] Conditionally disable client-side policy validation (#11550) --- .../policies/require-sso.component.ts | 14 ++++++++++++-- .../organizations/policies/single-org.component.ts | 14 ++++++++++++-- libs/common/src/enums/feature-flag.enum.ts | 2 ++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts b/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts index c969343670f..80335eb5d81 100644 --- a/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.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 { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -23,11 +25,19 @@ export class RequireSsoPolicy extends BasePolicy { templateUrl: "require-sso.component.html", }) export class RequireSsoPolicyComponent extends BasePolicyComponent { - constructor(private i18nService: I18nService) { + constructor( + private i18nService: I18nService, + private configService: ConfigService, + ) { super(); } - buildRequest(policiesEnabledMap: Map): Promise { + async buildRequest(policiesEnabledMap: Map): Promise { + if (await this.configService.getFeatureFlag(FeatureFlag.Pm13322AddPolicyDefinitions)) { + // We are now relying on server-side validation only + return super.buildRequest(policiesEnabledMap); + } + const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false; if (this.enabled.value && !singleOrgEnabled) { throw new Error(this.i18nService.t("requireSsoPolicyReqError")); diff --git a/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts b/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts index c0a1413aa06..9899c4b9193 100644 --- a/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.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 { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -18,11 +20,19 @@ export class SingleOrgPolicy extends BasePolicy { templateUrl: "single-org.component.html", }) export class SingleOrgPolicyComponent extends BasePolicyComponent { - constructor(private i18nService: I18nService) { + constructor( + private i18nService: I18nService, + private configService: ConfigService, + ) { super(); } - buildRequest(policiesEnabledMap: Map): Promise { + async buildRequest(policiesEnabledMap: Map): Promise { + if (await this.configService.getFeatureFlag(FeatureFlag.Pm13322AddPolicyDefinitions)) { + // We are now relying on server-side validation only + return super.buildRequest(policiesEnabledMap); + } + if (!this.enabled.value) { if (policiesEnabledMap.get(PolicyType.RequireSso) ?? false) { throw new Error( diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 45b02471f3c..d954f31c19f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -35,6 +35,7 @@ export enum FeatureFlag { PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api", AccessIntelligence = "pm-13227-access-intelligence", + Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -80,6 +81,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, [FeatureFlag.Pm3478RefactorOrganizationUserApi]: FALSE, [FeatureFlag.AccessIntelligence]: FALSE, + [FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 34e2d3dad2672ea6e552d27f1f430d35a419104f Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:34:44 -0400 Subject: [PATCH 12/74] PM-13318 - Fix missed page titles which didn't get converted to the new translation interface (#11572) --- apps/browser/src/popup/app-routing.module.ts | 8 ++++++-- apps/web/src/app/oss-routing.module.ts | 12 +++++++++--- .../providers/providers-routing.module.ts | 4 +++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 1e4e28ea6d0..0bf26f8c070 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -422,8 +422,12 @@ const routes: Routes = [ path: "hint", canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { - pageTitle: "requestPasswordHint", - pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + pageTitle: { + key: "requestPasswordHint", + }, + pageSubtitle: { + key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, pageIcon: UserLockIcon, showBackButton: true, state: "hint", diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e7ae154ec4e..fa4da88ce7d 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -184,7 +184,9 @@ const routes: Routes = [ path: "hint", canActivate: [unauthGuardFn()], data: { - pageTitle: "passwordHint", + pageTitle: { + key: "passwordHint", + }, titleId: "passwordHint", }, children: [ @@ -203,8 +205,12 @@ const routes: Routes = [ path: "hint", canActivate: [unauthGuardFn()], data: { - pageTitle: "requestPasswordHint", - pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + pageTitle: { + key: "requestPasswordHint", + }, + pageSubtitle: { + key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, pageIcon: UserLockIcon, state: "hint", }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 21fa863d2a9..aedb43fd8d0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -62,7 +62,9 @@ const routes: Routes = [ path: "accept-provider", component: AcceptProviderComponent, data: { - pageTitle: "joinProvider", + pageTitle: { + key: "joinProvider", + }, titleId: "acceptProvider", }, }, From d8f1527db003d62016b8911033cccfbc15180f6e Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:11:41 -0500 Subject: [PATCH 13/74] [PM-13016] Browser default match detection (#11569) * update conditional to only exit early if value is null - The UriMatchStrategy for Domain was 0 and hitting the conditional * add baseDomain test --- .../autofill-options/uri-option.component.spec.ts | 7 +++++++ .../components/autofill-options/uri-option.component.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts index fdb306ff761..d259566cc57 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.spec.ts @@ -58,6 +58,13 @@ describe("UriOptionComponent", () => { expect(component["uriMatchOptions"][0].label).toBe("default"); }); + it("should update the default uri match strategy label when it is domain", () => { + component.defaultMatchDetection = UriMatchStrategy.Domain; + fixture.detectChanges(); + + expect(component["uriMatchOptions"][0].label).toBe("defaultLabel baseDomain"); + }); + it("should update the default uri match strategy label", () => { component.defaultMatchDetection = UriMatchStrategy.Exact; fixture.detectChanges(); diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts index 82870befa12..4af80ed464c 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts @@ -84,7 +84,7 @@ export class UriOptionComponent implements ControlValueAccessor { @Input({ required: true }) set defaultMatchDetection(value: UriMatchStrategySetting) { // The default selection has a value of `null` avoid showing "Default (Default)" - if (!value) { + if (value === null) { return; } From 1f330b078dd9a3dc10a4ddf33b746f29b8b597e1 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 16 Oct 2024 09:52:45 -0400 Subject: [PATCH 14/74] Remove platformUtilService.showToast call (#11410) --- .../domain-add-edit-dialog.component.ts | 5 +++++ .../domain-verification/domain-verification.component.ts | 5 +++++ .../services/organization-domain/org-domain.service.spec.ts | 1 - .../services/organization-domain/org-domain.service.ts | 5 ----- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts index e0b76c7f5c3..7141f867882 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts @@ -105,6 +105,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { copyDnsTxt(): void { this.orgDomainService.copyDnsTxt(this.txtCtrl.value); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")), + }); } // End Form methods diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts index bc68bdaaf54..703808900c9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts @@ -101,6 +101,11 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { copyDnsTxt(dnsTxt: string): void { this.orgDomainService.copyDnsTxt(dnsTxt); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")), + }); } async verifyDomain(orgDomainId: string, domainName: string): Promise { diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts index 6721ea3a808..21027334fb8 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts @@ -180,6 +180,5 @@ describe("Org Domain Service", () => { it("copyDnsTxt copies DNS TXT to clipboard and shows toast", () => { orgDomainService.copyDnsTxt("fakeTxt"); expect(jest.spyOn(platformUtilService, "copyToClipboard")).toHaveBeenCalled(); - expect(jest.spyOn(platformUtilService, "showToast")).toHaveBeenCalled(); }); }); diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts b/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts index 5a5a2e4288f..ebdc098c855 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts @@ -23,11 +23,6 @@ export class OrgDomainService implements OrgDomainInternalServiceAbstraction { copyDnsTxt(dnsTxt: string): void { this.platformUtilsService.copyToClipboard(dnsTxt); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")), - ); } upsert(orgDomains: OrganizationDomainResponse[]): void { From d70d2cb995a9446bdb53283bce4b6b6b7a3507ed Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:21:54 -0700 Subject: [PATCH 15/74] [PM-13452] - add password health raw data component (#11519) * add raw data component * fix tests * simplify logic. fix tests * revert change to default config service * remove cipher report dep. fix tests. * revert changes to mock data and specs * remove mock data * use orgId param * fix test --- .../access-intelligence-routing.module.ts | 3 +- .../access-intelligence.component.html | 7 +- .../access-intelligence.component.ts | 2 + .../password-health.component.html | 57 +++++ .../password-health.component.spec.ts | 114 +++++++++ .../password-health.component.ts | 229 ++++++++++++++++++ 6 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.html create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.ts diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts index b35b1fa64a3..88efb2b4832 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { unauthGuardFn } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -11,7 +10,7 @@ const routes: Routes = [ { path: "", component: AccessIntelligenceComponent, - canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence), unauthGuardFn()], + canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)], data: { titleId: "accessIntelligence", }, diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html index 665f8f6b0c5..df3eee389f6 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html @@ -1,6 +1,9 @@ - + + + + diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts index 9e5eff6f629..8bdaadbd7e4 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts @@ -11,6 +11,7 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { ApplicationTableComponent } from "./application-table.component"; import { NotifiedMembersTableComponent } from "./notified-members-table.component"; +import { PasswordHealthComponent } from "./password-health.component"; export enum AccessIntelligenceTabType { AllApps = 0, @@ -26,6 +27,7 @@ export enum AccessIntelligenceTabType { CommonModule, JslibModule, HeaderModule, + PasswordHealthComponent, NotifiedMembersTableComponent, TabsModule, ], diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.html b/apps/web/src/app/tools/access-intelligence/password-health.component.html new file mode 100644 index 00000000000..32459706449 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.html @@ -0,0 +1,57 @@ + +

{{ "passwordsReportDesc" | i18n }}

+
+ + {{ "loading" | i18n }} +
+
+ + + + + {{ "name" | i18n }} + {{ "weakness" | i18n }} + {{ "timesReused" | i18n }} + {{ "timesExposed" | i18n }} + + + + + + + + + + {{ r.name }} + +
+ {{ r.subTitle }} + + + + {{ passwordStrengthMap.get(r.id)[0] | i18n }} + + + + + {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }} + + + + + {{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }} + + + +
+
+
+
diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts new file mode 100644 index 00000000000..4a6d5c50ee1 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -0,0 +1,114 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ActivatedRoute, convertToParamMap } from "@angular/router"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +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"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { TableModule } from "@bitwarden/components"; +import { TableBodyDirective } from "@bitwarden/components/src/table/table.component"; + +import { LooseComponentsModule } from "../../shared"; +import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; +// eslint-disable-next-line no-restricted-imports +import { cipherData } from "../reports/pages/reports-ciphers.mock"; + +import { PasswordHealthComponent } from "./password-health.component"; + +describe("PasswordHealthComponent", () => { + let component: PasswordHealthComponent; + let fixture: ComponentFixture; + let passwordStrengthService: MockProxy; + let organizationService: MockProxy; + let cipherServiceMock: MockProxy; + let auditServiceMock: MockProxy; + const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); + + beforeEach(async () => { + passwordStrengthService = mock(); + auditServiceMock = mock(); + organizationService = mock({ + get: jest.fn().mockResolvedValue({ id: "orgId" } as Organization), + }); + cipherServiceMock = mock({ + getAllFromApiForOrganization: jest.fn().mockResolvedValue(cipherData), + }); + + await TestBed.configureTestingModule({ + imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], + declarations: [TableBodyDirective], + providers: [ + { provide: CipherService, useValue: cipherServiceMock }, + { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService }, + { provide: OrganizationService, useValue: organizationService }, + { provide: I18nService, useValue: mock() }, + { provide: AuditService, useValue: auditServiceMock }, + { + provide: ActivatedRoute, + useValue: { + paramMap: of(activeRouteParams), + url: of([]), + }, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordHealthComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it("should populate reportCiphers with ciphers that have password issues", async () => { + passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 1 } as any); + + auditServiceMock.passwordLeaked.mockResolvedValue(5); + + await component.setCiphers(); + + const cipherIds = component.reportCiphers.map((c) => c.id); + + expect(cipherIds).toEqual([ + "cbea34a8-bde4-46ad-9d19-b05001228ab1", + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + ]); + expect(component.reportCiphers.length).toEqual(3); + }); + + it("should correctly populate passwordStrengthMap", async () => { + passwordStrengthService.getPasswordStrength.mockImplementation((password) => { + let score = 0; + if (password === "123") { + score = 1; + } else { + score = 4; + } + return { score } as any; + }); + + auditServiceMock.passwordLeaked.mockResolvedValue(0); + + await component.setCiphers(); + + expect(component.passwordStrengthMap.size).toBeGreaterThan(0); + expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([ + "veryWeak", + "danger", + ]); + expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([ + "veryWeak", + "danger", + ]); + }); +}); diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.ts new file mode 100644 index 00000000000..6e8e62c50db --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.ts @@ -0,0 +1,229 @@ +import { CommonModule } from "@angular/common"; +import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute } from "@angular/router"; +import { from, map, switchMap, tap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +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"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + BadgeVariant, + ContainerComponent, + TableDataSource, + TableModule, +} from "@bitwarden/components"; + +// 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({ + standalone: true, + selector: "tools-password-health", + templateUrl: "password-health.component.html", + imports: [ + BadgeModule, + OrganizationBadgeModule, + CommonModule, + ContainerComponent, + PipesModule, + JslibModule, + HeaderModule, + TableModule, + ], +}) +export class PasswordHealthComponent implements OnInit { + passwordStrengthMap = new Map(); + + weakPasswordCiphers: CipherView[] = []; + + passwordUseMap = new Map(); + + exposedPasswordMap = new Map(); + + dataSource = new TableDataSource(); + + reportCiphers: CipherView[] = []; + reportCipherIds: string[] = []; + + organization: Organization; + + loading = true; + + private destroyRef = inject(DestroyRef); + + constructor( + protected cipherService: CipherService, + protected passwordStrengthService: PasswordStrengthServiceAbstraction, + protected organizationService: OrganizationService, + protected auditService: AuditService, + protected i18nService: I18nService, + protected activatedRoute: ActivatedRoute, + ) {} + + ngOnInit() { + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap((organizationId) => { + return from(this.organizationService.get(organizationId)); + }), + tap((organization) => { + this.organization = organization; + }), + switchMap(() => from(this.setCiphers())), + ) + .subscribe(); + } + + async setCiphers() { + const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id); + allCiphers.forEach(async (cipher) => { + this.findWeakPassword(cipher); + this.findReusedPassword(cipher); + await this.findExposedPassword(cipher); + }); + this.dataSource.data = this.reportCiphers; + this.loading = false; + + // const reportIssues = allCiphers.map((c) => { + // if (this.passwordStrengthMap.has(c.id)) { + // return c; + // } + + // if (this.passwordUseMap.has(c.id)) { + // return c; + // } + + // if (this.exposedPasswordMap.has(c.id)) { + // return c; + // } + // }); + } + + protected checkForExistingCipher(ciph: CipherView) { + if (!this.reportCipherIds.includes(ciph.id)) { + this.reportCipherIds.push(ciph.id); + this.reportCiphers.push(ciph); + } + } + + protected async findExposedPassword(cipher: CipherView) { + const { type, login, isDeleted, edit, viewPassword, id } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + const exposedCount = await this.auditService.passwordLeaked(login.password); + if (exposedCount > 0) { + this.exposedPasswordMap.set(id, exposedCount); + this.checkForExistingCipher(cipher); + } + } + + protected findReusedPassword(cipher: CipherView) { + const { type, login, isDeleted, edit, viewPassword } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + if (this.passwordUseMap.has(login.password)) { + this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1); + } else { + this.passwordUseMap.set(login.password, 1); + } + + this.checkForExistingCipher(cipher); + } + + protected findWeakPassword(cipher: CipherView): void { + const { type, login, isDeleted, edit, viewPassword } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + const hasUserName = this.isUserNameNotEmpty(cipher); + let userInput: string[] = []; + if (hasUserName) { + const atPosition = login.username.indexOf("@"); + if (atPosition > -1) { + userInput = userInput + .concat( + login.username + .substring(0, atPosition) + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/), + ) + .filter((i) => i.length >= 3); + } else { + userInput = login.username + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/) + .filter((i) => i.length >= 3); + } + } + const { score } = this.passwordStrengthService.getPasswordStrength( + login.password, + null, + userInput.length > 0 ? userInput : null, + ); + + if (score != null && score <= 2) { + this.passwordStrengthMap.set(cipher.id, this.scoreKey(score)); + this.checkForExistingCipher(cipher); + } + } + + private isUserNameNotEmpty(c: CipherView): boolean { + return !Utils.isNullOrWhitespace(c.login.username); + } + + private scoreKey(score: number): [string, BadgeVariant] { + switch (score) { + case 4: + return ["strong", "success"]; + case 3: + return ["good", "primary"]; + case 2: + return ["weak", "warning"]; + default: + return ["veryWeak", "danger"]; + } + } +} From 742a8a33ddb27f81e503f466abafc9405d39281a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:12:12 -0400 Subject: [PATCH 16/74] [deps] Autofill: Update tldts to v6.1.52 (#11579) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index a609224dcb5..c5aeb306230 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.51", + "tldts": "6.1.52", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index f16c0c43947..3a0b189e282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.51", + "tldts": "6.1.52", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -225,7 +225,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.51", + "tldts": "6.1.52", "zxcvbn": "4.4.2" }, "bin": { @@ -36519,21 +36519,21 @@ } }, "node_modules/tldts": { - "version": "6.1.51", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz", - "integrity": "sha512-33lfQoL0JsDogIbZ8fgRyvv77GnRtwkNE/MOKocwUgPO1WrSfsq7+vQRKxRQZai5zd+zg97Iv9fpFQSzHyWdLA==", + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.52.tgz", + "integrity": "sha512-fgrDJXDjbAverY6XnIt0lNfv8A0cf7maTEaZxNykLGsLG7XP+5xhjBTrt/ieAsFjAlZ+G5nmXomLcZDkxXnDzw==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.51" + "tldts-core": "^6.1.52" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.51", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.51.tgz", - "integrity": "sha512-bu9oCYYWC1iRjx+3UnAjqCsfrWNZV1ghNQf49b3w5xE8J/tNShHTzp5syWJfwGH+pxUgTTLUnzHnfuydW7wmbg==", + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.52.tgz", + "integrity": "sha512-j4OxQI5rc1Ve/4m/9o2WhWSC4jGc4uVbCINdOEJRAraCi0YqTqgMcxUx7DbmuP0G3PCixoof/RZB0Q5Kh9tagw==", "license": "MIT" }, "node_modules/tmp": { diff --git a/package.json b/package.json index d0bc09412bb..86224e7277e 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.51", + "tldts": "6.1.52", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" From 80789465cb0b9646a89a484993a9ed9a1ef43899 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 16 Oct 2024 10:34:27 -0700 Subject: [PATCH 17/74] Fix vault item collection storybook (#11582) --- .../src/app/vault/components/vault-items/vault-items.stories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index 96089d2b156..f74b73b1030 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -288,6 +288,7 @@ function createCollectionView(i: number): CollectionAdminView { view.id = `collection-${i}`; view.name = `Collection ${i}`; view.organizationId = organization?.id; + view.manage = true; if (group !== undefined) { view.groups = [ From 6e370477762134a1b43ca730f8f0b40a3730d891 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:11:43 -0700 Subject: [PATCH 18/74] account for possible null value (#11589) --- .../send-form/components/options/send-options.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index bcda8b57107..4da3466f708 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -43,7 +43,7 @@

{{ "additionalOptions" | i18n }}

> {{ "sendPasswordDescV2" | i18n }}
- + Date: Wed, 16 Oct 2024 15:17:23 -0400 Subject: [PATCH 19/74] [PM-11613] Refactor personal-ownership.component to use reactive form (#11440) * Refactor personal-ownership.component to use reactive form * Refactor personal-ownership policy component to use base component form control --- .../policies/personal-ownership.component.html | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html index 85fb04730d4..2b6c86b1fdc 100644 --- a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html @@ -2,15 +2,7 @@ {{ "personalOwnershipExemption" | i18n }} -
-
- - -
-
+ + + {{ "turnOn" | i18n }} + From 0f525fa9bc356b16052e0c79171d28b3499a40a0 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:05:11 -0400 Subject: [PATCH 20/74] [PM-11778] Removed super syntax and replaced with this (#11511) --- apps/browser/src/auth/popup/hint.component.ts | 2 +- apps/browser/src/auth/popup/lock.component.ts | 2 +- .../auth/popup/login-via-auth-request.component.ts | 2 +- apps/browser/src/auth/popup/login.component.ts | 4 ++-- apps/browser/src/auth/popup/sso.component.ts | 6 +++--- .../src/auth/popup/two-factor-auth.component.ts | 4 ++-- apps/browser/src/auth/popup/two-factor.component.ts | 12 ++++++------ .../auth/login/login-via-auth-request.component.ts | 2 +- apps/desktop/src/auth/login/login.component.ts | 2 +- apps/desktop/src/auth/sso.component.ts | 4 ++-- apps/desktop/src/auth/two-factor.component.ts | 4 ++-- .../auth/register-form/register-form.component.ts | 2 +- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts index bc1f68f4c43..e97236fe6a8 100644 --- a/apps/browser/src/auth/popup/hint.component.ts +++ b/apps/browser/src/auth/popup/hint.component.ts @@ -34,7 +34,7 @@ export class HintComponent extends BaseHintComponent { toastService, ); - super.onSuccessfulSubmit = async () => { + this.onSuccessfulSubmit = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate([this.successRoute]); diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 96bda7012d1..75fcfc58f6a 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -105,7 +105,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; - super.onSuccessfulSubmit = async () => { + this.onSuccessfulSubmit = async () => { const previousUrl = this.routerService.getPreviousUrl(); if (previousUrl) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index 53f29badee6..33ec2acf387 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -74,7 +74,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { loginStrategyService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { await syncService.fullSync(true); }; } diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index ea72fb61f5f..fd4d9bc547a 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -78,10 +78,10 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { registerRouteService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { await syncService.fullSync(true); }; - super.successRoute = "/tabs/vault"; + this.successRoute = "/tabs/vault"; this.showPasswordless = flagEnabled("showPasswordless"); } diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 42222c42b97..988563c2fe6 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -79,7 +79,7 @@ export class SsoComponent extends BaseSsoComponent { }); this.clientId = "browser"; - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); @@ -92,13 +92,13 @@ export class SsoComponent extends BaseSsoComponent { this.win.close(); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); }; - super.onSuccessfulLoginTdeNavigate = async () => { + this.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); }; } diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 27c95321100..9e755746e6f 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -118,7 +118,7 @@ export class TwoFactorAuthComponent win, toastService, ); - super.onSuccessfulLoginTdeNavigate = async () => { + this.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); }; this.onSuccessfulLoginNavigate = this.goAfterLogIn; @@ -131,7 +131,7 @@ export class TwoFactorAuthComponent // WebAuthn fallback response this.selectedProviderType = TwoFactorProviderType.WebAuthn; this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index e9167a5087a..27c4604be91 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -87,23 +87,23 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit accountService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); }; - super.onSuccessfulLoginTdeNavigate = async () => { + this.onSuccessfulLoginTdeNavigate = async () => { this.win.close(); }; - super.successRoute = "/tabs/vault"; + this.successRoute = "/tabs/vault"; // FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe this.webAuthnNewTab = true; } @@ -113,7 +113,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // WebAuthn fallback response this.selectedProviderType = TwoFactorProviderType.WebAuthn; this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.syncService.fullSync(true); @@ -155,7 +155,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (qParams) => { if (qParams.sso === "true") { - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // This is not awaited so we don't pause the application while the sync is happening. // This call is executed by the service that lives in the background script so it will continue // the sync even if this tab closes. diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index c0a6a51b907..12be2f01c08 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -83,7 +83,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { toastService, ); - super.onSuccessfulLogin = () => { + this.onSuccessfulLogin = () => { return syncService.fullSync(true); }; } diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index b43e5bc84f0..6ba143421ca 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -99,7 +99,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest registerRouteService, toastService, ); - super.onSuccessfulLogin = () => { + this.onSuccessfulLogin = () => { return syncService.fullSync(true); }; } diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 6821a548945..760eef14e80 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -65,13 +65,13 @@ export class SsoComponent extends BaseSsoComponent { accountService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index d2c5efe929f..0050ec65608 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -89,13 +89,13 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest accountService, toastService, ); - super.onSuccessfulLogin = async () => { + this.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); }; - super.onSuccessfulLoginTde = async () => { + this.onSuccessfulLoginTde = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises syncService.fullSync(true); diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts index bf4a3e8203f..9982af2ab5d 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.ts @@ -71,7 +71,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn dialogService, toastService, ); - super.modifyRegisterRequest = async (request: RegisterRequest) => { + this.modifyRegisterRequest = async (request: RegisterRequest) => { // Org invites are deep linked. Non-existent accounts are redirected to the register page. // Org user id and token are included here only for validation and two factor purposes. const orgInvite = await acceptOrgInviteService.getOrganizationInvite(); From 80d0f7e385ddfadbe8c18b99f3327182b8f21715 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Wed, 16 Oct 2024 15:26:16 -0500 Subject: [PATCH 21/74] [PM-13768] Create account fields are no longer showing the inline menu when identities are turned off (#11592) --- .../src/autofill/services/autofill-overlay-content.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 1f0a38ad806..4109662fd66 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -1158,7 +1158,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ } if ( - this.showInlineMenuIdentities && this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm( autofillFieldData, pageDetails, From 84d592a08036839ffee02ace97fa06113e1c47a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 22:34:59 +0200 Subject: [PATCH 22/74] [deps] Vault: Update @koa/router to v13 (#10602) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: SmithThe4th --- apps/cli/package.json | 2 +- package-lock.json | 18 ++++++++---------- package.json | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index c5aeb306230..a45e88acfa2 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "argon2": "0.40.1", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", diff --git a/package-lock.json b/package-lock.json index 3a0b189e282..753d8975573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@bitwarden/sdk-internal": "0.1.3", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "11.2.0", @@ -202,7 +202,7 @@ "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "argon2": "0.40.1", "big-integer": "1.6.52", "browser-hrtime": "1.1.8", @@ -7021,20 +7021,17 @@ } }, "node_modules/@koa/router": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz", - "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==", - "deprecated": "Use v12.0.2 or higher to fix the vulnerability issue", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", + "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", "license": "MIT", "dependencies": { - "debug": "^4.3.4", "http-errors": "^2.0.0", "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^6.3.0" }, "engines": { - "node": ">= 12" + "node": ">= 18" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -27277,6 +27274,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" diff --git a/package.json b/package.json index 86224e7277e..04b33e77099 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@bitwarden/sdk-internal": "0.1.3", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", - "@koa/router": "12.0.1", + "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", "@ng-select/ng-select": "11.2.0", From e256bde1deb33fbb589915d6b1e532ebb34cf58e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 22:35:45 +0200 Subject: [PATCH 23/74] [deps] Vault: Update koa to v2.15.3 (#10567) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: SmithThe4th --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index a45e88acfa2..55bcee689d0 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -68,7 +68,7 @@ "inquirer": "8.2.6", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", diff --git a/package-lock.json b/package-lock.json index 753d8975573..07210a6013a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "jquery": "3.7.1", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -103,7 +103,7 @@ "@types/jest": "29.5.12", "@types/jquery": "3.5.30", "@types/jsdom": "21.1.7", - "@types/koa": "2.14.0", + "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", @@ -213,7 +213,7 @@ "inquirer": "8.2.6", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -9521,9 +9521,9 @@ } }, "node_modules/@types/koa": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.14.0.tgz", - "integrity": "sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", "dev": true, "license": "MIT", "dependencies": { @@ -25240,9 +25240,9 @@ } }, "node_modules/koa": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.0.tgz", - "integrity": "sha512-KEL/vU1knsoUvfP4MC4/GthpQrY/p6dzwaaGI6Rt4NQuFqkw3qrvsdYF5pz3wOfi7IGTvMPHC9aZIcUKYFNxsw==", + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", "license": "MIT", "dependencies": { "accepts": "^1.3.5", diff --git a/package.json b/package.json index 04b33e77099..eb871be7766 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@types/jest": "29.5.12", "@types/jquery": "3.5.30", "@types/jsdom": "21.1.7", - "@types/koa": "2.14.0", + "@types/koa": "2.15.0", "@types/koa__multer": "2.0.7", "@types/koa__router": "12.0.4", "@types/koa-bodyparser": "4.3.7", @@ -181,7 +181,7 @@ "jquery": "3.7.1", "jsdom": "25.0.1", "jszip": "3.10.1", - "koa": "2.15.0", + "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", From 4b67cd24b451255981218cb5e9436b23fbbda5e7 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:28:27 -0400 Subject: [PATCH 24/74] Auth/PM-8112 - UI refresh - Registration Components (#11353) * PM-8112 - Update classes of existing registration icons * PM-8112 - Add new icons * PM-8112 - Export icons from libs/auth * PM-8112 - RegistrationStart - Add new user icon as page icon * PM-8112 - Replace RegistrationCheckEmailIcon with new icon so it displays properly * PM-8112 - RegistrationFinish - Add new icon across clients * PM-8112 - Registration start comp - update page icon and page title on state change to match figma * PM-8112 - RegistrationFinish - adding most of framework for changing page title & subtitle when an org invite is in state. * PM-8112 - Add joinOrganizationName to all clients translations * PM-8112 - RegistrationFinish - Remove default page title & subtitle and let onInit logic figure out what to set based on flows. * PM-8112 - RegistrationStart - Fix setAnonLayoutWrapperData calls * PM-8112 - RegistrationFinish - simplify qParams init logic to make handling loading and page title and subtitle setting easier. * PM-8112 - Registration Link expired - move icon to page icon out of main content * PM-8112 - RegistrationFinish - Refactor init logic further into distinct flows. * PM-8112 - Fix icons * PM-8112 - Extension AppRoutingModule - move sign up start & finish routes under extension anon layout * PM-8112 - Fix storybook * PM-8112 - Clean up unused prop * PM-8112 - RegistrationLockAltIcon tweaks * PM-8112 - Update icons to have proper styling * PM-8112 - RegistrationUserAddIcon - remove unnecessary svg class * PM-8112 - Fix icons --- apps/browser/src/_locales/en/messages.json | 9 ++ apps/browser/src/popup/app-routing.module.ts | 64 ++++----- apps/desktop/src/app/app-routing.module.ts | 10 +- apps/desktop/src/locales/en/messages.json | 9 ++ .../web-registration-finish.service.spec.ts | 30 +++++ .../web-registration-finish.service.ts | 9 ++ apps/web/src/app/oss-routing.module.ts | 12 +- apps/web/src/locales/en/messages.json | 9 ++ libs/auth/src/angular/icons/index.ts | 3 + .../icons/registration-check-email.icon.ts | 29 +++-- .../icons/registration-expired-link.icon.ts | 21 +-- .../icons/registration-lock-alt.icon.ts | 41 ++++++ .../icons/registration-user-add.icon.ts | 24 ++++ ...efault-registration-finish.service.spec.ts | 8 ++ .../default-registration-finish.service.ts | 4 + .../registration-finish.component.ts | 123 +++++++++++------- .../registration-finish.service.ts | 9 +- .../registration-link-expired.component.html | 2 - .../registration-start.component.html | 13 -- .../registration-start.component.ts | 16 ++- .../registration-start.stories.ts | 10 ++ 21 files changed, 325 insertions(+), 130 deletions(-) create mode 100644 libs/auth/src/angular/icons/registration-lock-alt.icon.ts create mode 100644 libs/auth/src/angular/icons/registration-user-add.icon.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 25386222d4e..290663f4347 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -71,6 +71,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 0bf26f8c070..1a95ad74839 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -20,9 +20,11 @@ import { LockV2Component, PasswordHintComponent, RegistrationFinishComponent, + RegistrationLockAltIcon, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, + RegistrationUserAddIcon, SetPasswordJitComponent, UserLockIcon, } from "@bitwarden/auth/angular"; @@ -447,40 +449,18 @@ const routes: Routes = [ { path: "", component: ExtensionAnonLayoutWrapperComponent, - children: [ - { - path: "lockV2", - canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], - data: { - pageIcon: LockIcon, - pageTitle: { - key: "yourVaultIsLockedV2", - }, - showReadonlyHostname: true, - showAcctSwitcher: true, - } satisfies ExtensionAnonLayoutWrapperData, - children: [ - { - path: "", - component: LockV2Component, - }, - ], - }, - ], - }, - { - path: "", - component: AnonLayoutWrapperComponent, children: [ { path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { state: "signup", + pageIcon: RegistrationUserAddIcon, pageTitle: { key: "createAccount", }, - } satisfies RouteDataProperties & AnonLayoutWrapperData, + showBackButton: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, children: [ { path: "", @@ -500,14 +480,10 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: { - key: "setAStrongPassword", - }, - pageSubtitle: { - key: "finishCreatingYourAccountBySettingAPassword", - }, + pageIcon: RegistrationLockAltIcon, state: "finish-signup", - } satisfies RouteDataProperties & AnonLayoutWrapperData, + showBackButton: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, children: [ { path: "", @@ -515,6 +491,30 @@ const routes: Routes = [ }, ], }, + { + path: "lockV2", + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + data: { + pageIcon: LockIcon, + pageTitle: { + key: "yourVaultIsLockedV2", + }, + showReadonlyHostname: true, + showAcctSwitcher: true, + } satisfies ExtensionAnonLayoutWrapperData, + children: [ + { + path: "", + component: LockV2Component, + }, + ], + }, + ], + }, + { + path: "", + component: AnonLayoutWrapperComponent, + children: [ { path: "set-password-jit", canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index e8ae31e78a8..8b8f62047f0 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -19,9 +19,11 @@ import { LockV2Component, PasswordHintComponent, RegistrationFinishComponent, + RegistrationLockAltIcon, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, + RegistrationUserAddIcon, SetPasswordJitComponent, UserLockIcon, } from "@bitwarden/auth/angular"; @@ -169,6 +171,7 @@ const routes: Routes = [ path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { + pageIcon: RegistrationUserAddIcon, pageTitle: { key: "createAccount", }, @@ -192,12 +195,7 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: { - key: "setAStrongPassword", - }, - pageSubtitle: { - key: "finishCreatingYourAccountBySettingAPassword", - }, + pageIcon: RegistrationLockAltIcon, } satisfies AnonLayoutWrapperData, children: [ { diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 4647853f715..9924a91fa36 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -602,6 +602,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 2faf3f85d10..45eb3c5c0d4 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -52,6 +52,36 @@ describe("DefaultRegistrationFinishService", () => { expect(service).not.toBeFalsy(); }); + describe("getOrgNameFromOrgInvite()", () => { + let orgInvite: OrganizationInvite | null; + + beforeEach(() => { + orgInvite = new OrganizationInvite(); + orgInvite.organizationId = "organizationId"; + orgInvite.organizationUserId = "organizationUserId"; + orgInvite.token = "orgInviteToken"; + orgInvite.email = "email"; + }); + + it("returns null when the org invite is null", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.getOrgNameFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + }); + + it("returns the organization name from the organization invite when it exists", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + const result = await service.getOrgNameFromOrgInvite(); + + expect(result).toEqual(orgInvite.organizationName); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + }); + }); + describe("getMasterPasswordPolicyOptsFromOrgInvite()", () => { let orgInvite: OrganizationInvite | null; diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index 5239601bbcd..560196dd195 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -32,6 +32,15 @@ export class WebRegistrationFinishService super(cryptoService, accountApiService); } + override async getOrgNameFromOrgInvite(): Promise { + const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + if (orgInvite == null) { + return null; + } + + return orgInvite.organizationName; + } + override async getMasterPasswordPolicyOptsFromOrgInvite(): Promise { // If there's a deep linked org invite, use it to get the password policies const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index fa4da88ce7d..8822800b36d 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -25,6 +25,9 @@ import { LockV2Component, LockIcon, UserLockIcon, + RegistrationUserAddIcon, + RegistrationLockAltIcon, + RegistrationExpiredLinkIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -234,6 +237,7 @@ const routes: Routes = [ path: "signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { + pageIcon: RegistrationUserAddIcon, pageTitle: { key: "createAccount", }, @@ -258,12 +262,7 @@ const routes: Routes = [ path: "finish-signup", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { - pageTitle: { - key: "setAStrongPassword", - }, - pageSubtitle: { - key: "finishCreatingYourAccountBySettingAPassword", - }, + pageIcon: RegistrationLockAltIcon, titleId: "setAStrongPassword", } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -310,6 +309,7 @@ const routes: Routes = [ path: "signup-link-expired", canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], data: { + pageIcon: RegistrationExpiredLinkIcon, pageTitle: { key: "expiredLink", }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 96e987a7f64..5125bb1bfe0 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3781,6 +3781,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "joinOrganizationDesc": { "message": "You've been invited to join the organization listed above. To accept the invitation, you need to log in or create a new Bitwarden account." }, diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index cfcad992e34..26e668b7841 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -3,3 +3,6 @@ export * from "./bitwarden-shield.icon"; export * from "./lock.icon"; export * from "./user-lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; +export * from "./registration-user-add.icon"; +export * from "./registration-lock-alt.icon"; +export * from "./registration-expired-link.icon"; diff --git a/libs/auth/src/angular/icons/registration-check-email.icon.ts b/libs/auth/src/angular/icons/registration-check-email.icon.ts index 1d173ff585f..6f7dd6a2d63 100644 --- a/libs/auth/src/angular/icons/registration-check-email.icon.ts +++ b/libs/auth/src/angular/icons/registration-check-email.icon.ts @@ -1,12 +1,23 @@ import { svgIcon } from "@bitwarden/components"; export const RegistrationCheckEmailIcon = svgIcon` - - - - - - - - -`; + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/registration-expired-link.icon.ts b/libs/auth/src/angular/icons/registration-expired-link.icon.ts index 50bcc53808f..3323c7f0b2b 100644 --- a/libs/auth/src/angular/icons/registration-expired-link.icon.ts +++ b/libs/auth/src/angular/icons/registration-expired-link.icon.ts @@ -1,20 +1,9 @@ import { svgIcon } from "@bitwarden/components"; export const RegistrationExpiredLinkIcon = svgIcon` - - - - - - - - - - + + + `; diff --git a/libs/auth/src/angular/icons/registration-lock-alt.icon.ts b/libs/auth/src/angular/icons/registration-lock-alt.icon.ts new file mode 100644 index 00000000000..511f9710dc6 --- /dev/null +++ b/libs/auth/src/angular/icons/registration-lock-alt.icon.ts @@ -0,0 +1,41 @@ +import { svgIcon } from "@bitwarden/components"; + +export const RegistrationLockAltIcon = svgIcon` + + + + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/registration-user-add.icon.ts b/libs/auth/src/angular/icons/registration-user-add.icon.ts new file mode 100644 index 00000000000..69240cd0298 --- /dev/null +++ b/libs/auth/src/angular/icons/registration-user-add.icon.ts @@ -0,0 +1,24 @@ +import { svgIcon } from "@bitwarden/components"; + +export const RegistrationUserAddIcon = svgIcon` + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index 5417d617a9a..fe6b9b2c7dc 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -37,6 +37,14 @@ describe("DefaultRegistrationFinishService", () => { }); }); + describe("getOrgNameFromOrgInvite()", () => { + it("returns null", async () => { + const result = await service.getOrgNameFromOrgInvite(); + + expect(result).toBeNull(); + }); + }); + describe("finishRegistration()", () => { let email: string; let emailVerificationToken: string; diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 63b01be9953..6d77c777491 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -15,6 +15,10 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi protected accountApiService: AccountApiService, ) {} + getOrgNameFromOrgInvite(): Promise { + return null; + } + getMasterPasswordPolicyOptsFromOrgInvite(): Promise { return null; } diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index ef40d95dce9..be9d8abe5b0 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Params, Router, RouterModule } from "@angular/router"; -import { EMPTY, Subject, from, switchMap, takeUntil, tap } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; @@ -15,6 +15,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid import { ToastService } from "@bitwarden/components"; import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "../../../common"; +import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { InputPasswordComponent } from "../../input-password/input-password.component"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -60,55 +61,72 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { private accountApiService: AccountApiService, private loginStrategyService: LoginStrategyServiceAbstraction, private logService: LogService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) {} async ngOnInit() { - this.listenForQueryParamChanges(); - this.masterPasswordPolicyOptions = - await this.registrationFinishService.getMasterPasswordPolicyOptsFromOrgInvite(); + const qParams = await firstValueFrom(this.activatedRoute.queryParams); + this.handleQueryParams(qParams); + + if ( + qParams.fromEmail && + qParams.fromEmail === "true" && + this.email && + this.emailVerificationToken + ) { + await this.initEmailVerificationFlow(); + } else { + // Org Invite flow OR registration with email verification disabled Flow + const orgInviteFlow = await this.initOrgInviteFlowIfPresent(); + + if (!orgInviteFlow) { + this.initRegistrationWithEmailVerificationDisabledFlow(); + } + } + + this.loading = false; } - private listenForQueryParamChanges() { - this.activatedRoute.queryParams - .pipe( - tap((qParams: Params) => { - if (qParams.email != null && qParams.email.indexOf("@") > -1) { - this.email = qParams.email; - } + private handleQueryParams(qParams: Params) { + if (qParams.email != null && qParams.email.indexOf("@") > -1) { + this.email = qParams.email; + } - if (qParams.token != null) { - this.emailVerificationToken = qParams.token; - } + if (qParams.token != null) { + this.emailVerificationToken = qParams.token; + } - if (qParams.orgSponsoredFreeFamilyPlanToken != null) { - this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken; - } + if (qParams.orgSponsoredFreeFamilyPlanToken != null) { + this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken; + } - if (qParams.acceptEmergencyAccessInviteToken != null && qParams.emergencyAccessId) { - this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken; - this.emergencyAccessId = qParams.emergencyAccessId; - } - }), - switchMap((qParams: Params) => { - if ( - qParams.fromEmail && - qParams.fromEmail === "true" && - this.email && - this.emailVerificationToken - ) { - return from( - this.registerVerificationEmailClicked(this.email, this.emailVerificationToken), - ); - } else { - // org invite flow - this.loading = false; - return EMPTY; - } - }), + if (qParams.acceptEmergencyAccessInviteToken != null && qParams.emergencyAccessId) { + this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken; + this.emergencyAccessId = qParams.emergencyAccessId; + } + } + + private async initOrgInviteFlowIfPresent(): Promise { + this.masterPasswordPolicyOptions = + await this.registrationFinishService.getMasterPasswordPolicyOptsFromOrgInvite(); + + const orgName = await this.registrationFinishService.getOrgNameFromOrgInvite(); + if (orgName) { + // Org invite exists + // Set the page title and subtitle appropriately + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { + key: "joinOrganizationName", + placeholders: [orgName], + }, + pageSubtitle: { + key: "finishJoiningThisOrganizationBySettingAMasterPassword", + }, + }); + return true; + } - takeUntil(this.destroy$), - ) - .subscribe(); + return false; } async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { @@ -162,9 +180,24 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.submitting = false; } + private setDefaultPageTitleAndSubtitle() { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { + key: "setAStrongPassword", + }, + pageSubtitle: { + key: "finishCreatingYourAccountBySettingAPassword", + }, + }); + } + + private async initEmailVerificationFlow() { + this.setDefaultPageTitleAndSubtitle(); + await this.registerVerificationEmailClicked(this.email, this.emailVerificationToken); + } + private async registerVerificationEmailClicked(email: string, emailVerificationToken: string) { const request = new RegisterVerificationEmailClickedRequest(email, emailVerificationToken); - try { const result = await this.accountApiService.registerVerificationEmailClicked(request); @@ -174,11 +207,9 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { message: this.i18nService.t("emailVerifiedV2"), variant: "success", }); - this.loading = false; } } catch (e) { await this.handleRegisterVerificationEmailClickedError(e); - this.loading = false; } } @@ -204,6 +235,10 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { } } + private initRegistrationWithEmailVerificationDisabledFlow() { + this.setDefaultPageTitleAndSubtitle(); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index b585aa78ed6..b7abd381084 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -3,6 +3,13 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { PasswordInputResult } from "../../input-password/password-input-result"; export abstract class RegistrationFinishService { + /** + * Retrieves the organization name from an organization invite if it exists. + * Organization invites can currently only be accepted on the web. + * @returns a promise which resolves to the organization name string or null if no invite exists. + */ + abstract getOrgNameFromOrgInvite(): Promise; + /** * Gets the master password policy options from an organization invite if it exits. * Organization invites can currently only be accepted on the web. @@ -18,7 +25,7 @@ export abstract class RegistrationFinishService { * @param orgSponsoredFreeFamilyPlanToken The optional org sponsored free family plan token. * @param acceptEmergencyAccessInviteToken The optional accept emergency access invite token. * @param emergencyAccessId The optional emergency access id which is required to validate the emergency access invite token. - * Returns a promise which resolves to the captcha bypass token string upon a successful account creation. + * @returns a promise which resolves to the captcha bypass token string upon a successful account creation. */ abstract finishRegistration( email: string, diff --git a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html index 5aa6866bbe0..77149902310 100644 --- a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html +++ b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.html @@ -1,6 +1,4 @@
- -

- - -

- {{ "checkYourEmail" | i18n }} -

-

{{ "apiKey" | i18n }}

{{ "collectionManagement" | i18n }}

{{ "collectionManagementDesc" | i18n }}

@@ -60,12 +64,24 @@

{{ "collectionManagement" | i1 {{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }} - - {{ "limitCollectionCreationDeletionDesc" | i18n }} - - + + + {{ "limitCollectionCreationDesc" | i18n }} + + + + {{ "limitCollectionDeletionDesc" | i18n }} + + + + + + {{ "limitCollectionCreationDeletionDesc" | i18n }} + + + - - - - -

-
-

{{ "noPasswordsInList" | i18n }}

-
+ + + + +
+
+

{{ "noPasswordsInList" | i18n }}

+
diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts new file mode 100644 index 00000000000..8772a245821 --- /dev/null +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -0,0 +1,97 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { BehaviorSubject } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { ColorPasswordModule, ItemModule, ToastService } from "@bitwarden/components"; +import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; + +import { PasswordHistoryViewComponent } from "./password-history-view.component"; + +describe("PasswordHistoryViewComponent", () => { + let component: PasswordHistoryViewComponent; + let fixture: ComponentFixture; + + const mockCipher = { + id: "122-333-444", + type: CipherType.Login, + organizationId: "222-444-555", + } as CipherView; + + const copyToClipboard = jest.fn(); + const showToast = jest.fn(); + const activeAccount$ = new BehaviorSubject<{ id: string }>({ id: "666-444-444" }); + const mockCipherService = { + get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }), + getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), + }; + + beforeEach(async () => { + mockCipherService.get.mockClear(); + mockCipherService.getKeyForCipherKeyDecryption.mockClear(); + copyToClipboard.mockClear(); + showToast.mockClear(); + + await TestBed.configureTestingModule({ + imports: [ItemModule, ColorPasswordModule, JslibModule], + providers: [ + { provide: WINDOW, useValue: window }, + { provide: CipherService, useValue: mockCipherService }, + { provide: PlatformUtilsService, useValue: { copyToClipboard } }, + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: ToastService, useValue: { showToast } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PasswordHistoryViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("renders no history text when history does not exist", () => { + expect(fixture.debugElement.nativeElement.textContent).toBe("noPasswordsInList"); + }); + + describe("history", () => { + const password1 = { password: "bad-password-1", lastUsedDate: new Date("09/13/2004") }; + const password2 = { password: "bad-password-2", lastUsedDate: new Date("02/01/2004") }; + + beforeEach(async () => { + mockCipher.passwordHistory = [password1, password2]; + + mockCipherService.get.mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }); + await component.ngOnInit(); + fixture.detectChanges(); + }); + + it("renders all passwords", () => { + const passwords = fixture.debugElement.queryAll(By.directive(ColorPasswordComponent)); + + expect(passwords.map((password) => password.componentInstance.password)).toEqual([ + "bad-password-1", + "bad-password-2", + ]); + }); + + it("copies a password", () => { + const copyButton = fixture.debugElement.query(By.css("button")); + + copyButton.nativeElement.click(); + + expect(copyToClipboard).toHaveBeenCalledWith("bad-password-1", { window: window }); + expect(showToast).toHaveBeenCalledWith({ + message: "passwordCopied", + title: "", + variant: "info", + }); + }); + }); +}); diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.ts b/libs/vault/src/components/password-history-view/password-history-view.component.ts new file mode 100644 index 00000000000..5e858af7275 --- /dev/null +++ b/libs/vault/src/components/password-history-view/password-history-view.component.ts @@ -0,0 +1,77 @@ +import { CommonModule } from "@angular/common"; +import { OnInit, Inject, Component, Input } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; +import { + ToastService, + ItemModule, + ColorPasswordModule, + IconButtonModule, +} from "@bitwarden/components"; + +@Component({ + selector: "vault-password-history-view", + templateUrl: "./password-history-view.component.html", + standalone: true, + imports: [CommonModule, ItemModule, ColorPasswordModule, IconButtonModule, JslibModule], +}) +export class PasswordHistoryViewComponent implements OnInit { + /** + * The ID of the cipher to display the password history for. + */ + @Input({ required: true }) cipherId: CipherId; + + /** The password history for the cipher. */ + history: PasswordHistoryView[] = []; + + constructor( + @Inject(WINDOW) private win: Window, + protected cipherService: CipherService, + protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, + protected accountService: AccountService, + protected toastService: ToastService, + ) {} + + async ngOnInit() { + await this.init(); + } + + /** Copies a password to the clipboard. */ + copy(password: string) { + const copyOptions = this.win != null ? { window: this.win } : undefined; + this.platformUtilsService.copyToClipboard(password, copyOptions); + this.toastService.showToast({ + variant: "info", + title: "", + message: this.i18nService.t("passwordCopied"), + }); + } + + /** Retrieve the password history for the given cipher */ + protected async init() { + const cipher = await this.cipherService.get(this.cipherId); + const activeAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)), + ); + + if (!activeAccount?.id) { + throw new Error("Active account is not available."); + } + + const activeUserId = activeAccount.id as UserId; + const decCipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + + this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; + } +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index d5841c7db06..f6a95281f81 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -12,5 +12,6 @@ export { } from "./components/assign-collections.component"; export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component"; +export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component"; export * as VaultIcons from "./icons"; From 80a4fba7871d2133a5f799c52686adcfe84e10d2 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:57:34 -0500 Subject: [PATCH 51/74] Allow for the web generation dialog to disable the margin of the tools generation components (#11565) --- .../web-generator-dialog/web-generator-dialog.component.html | 1 + .../components/src/passphrase-settings.component.html | 2 +- .../generator/components/src/passphrase-settings.component.ts | 4 ++++ .../components/src/password-generator.component.html | 2 ++ .../generator/components/src/password-generator.component.ts | 4 ++++ .../generator/components/src/password-settings.component.html | 2 +- .../generator/components/src/password-settings.component.ts | 4 ++++ .../components/src/username-generator.component.html | 4 ++-- .../generator/components/src/username-generator.component.ts | 4 ++++ .../cipher-generator/cipher-form-generator.component.html | 2 ++ .../cipher-generator/cipher-form-generator.component.ts | 4 ++++ 11 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.html b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.html index afe62cdc8a2..e224d1d19cc 100644 --- a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.html +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.html @@ -6,6 +6,7 @@ diff --git a/libs/tools/generator/components/src/passphrase-settings.component.html b/libs/tools/generator/components/src/passphrase-settings.component.html index c40df97c69c..2a3f4b5a287 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.html +++ b/libs/tools/generator/components/src/passphrase-settings.component.html @@ -1,4 +1,4 @@ - +
{{ "options" | i18n }}
diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index 25e028210cc..82524eba4d8 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -1,3 +1,4 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, skip, takeUntil, Subject } from "rxjs"; @@ -47,6 +48,9 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { @Input() showHeader: boolean = true; + /** Removes bottom margin from `bit-section` */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index 9a33aa143ec..b4cf8c6cdb6 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -32,6 +32,7 @@ class="tw-mt-6" *ngIf="(algorithm$ | async)?.id === 'password'" [userId]="this.userId$ | async" + [disableMargin]="disableMargin" (onUpdated)="generate$.next()" /> diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index bf33c7cfca9..e3f9073cb1e 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -1,3 +1,4 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { BehaviorSubject, @@ -45,6 +46,9 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { @Input() userId: UserId | null; + /** Removes bottom margin, passed to downstream components */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** tracks the currently selected credential type */ protected credentialType$ = new BehaviorSubject(null); diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index fcafc789049..9c4fb595392 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -1,4 +1,4 @@ - +

{{ "options" | i18n }}

diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index 9466c81a0f4..2a8bff31c4a 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -1,3 +1,4 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, takeUntil, Subject, map, filter, tap, debounceTime, skip } from "rxjs"; @@ -55,6 +56,9 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { @Input() waitMs: number = 100; + /** Removes bottom margin from `bit-section` */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index 6425cb7a38f..e9d7d1c1f8c 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -17,11 +17,11 @@
- +
{{ "options" | i18n }}
-
+
diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index 767c73c398a..fd1a21cc3e9 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -1,3 +1,4 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { @@ -57,6 +58,9 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { @Output() readonly onGenerated = new EventEmitter(); + /** Removes bottom margin from internal elements */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** Tracks the selected generation algorithm */ protected credential = this.formBuilder.group({ type: [null as CredentialAlgorithm], diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html index 181ca50da8a..445908679c3 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html @@ -1,8 +1,10 @@ diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index db6e9ae106b..79fac29d4d9 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -1,3 +1,4 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; @@ -21,6 +22,9 @@ export class CipherFormGeneratorComponent { @Input({ required: true }) type: "password" | "username"; + /** Removes bottom margin of internal sections */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** * Emits an event when a new value is generated. */ From 82547573752a0e5ecbdcd8094c45207a0a46bc48 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:28:42 -0700 Subject: [PATCH 52/74] [PM-13453] - Health report raw data with member count component (#11573) * add raw data + members table * remove commented code --- .../access-intelligence.component.html | 3 + .../access-intelligence.component.ts | 2 + .../password-health-members.component.html | 61 +++++ .../password-health-members.component.ts | 233 ++++++++++++++++++ .../password-health.mock.ts | 66 +++++ 5 files changed, 365 insertions(+) create mode 100644 apps/web/src/app/tools/access-intelligence/password-health-members.component.html create mode 100644 apps/web/src/app/tools/access-intelligence/password-health-members.component.ts create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.mock.ts diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html index df3eee389f6..78ddfb23929 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html @@ -3,6 +3,9 @@ + + + - {{ "noMatchingLoginsForSite" | i18n }} - {{ "searchSavePasskeyNewLogin" | i18n }} + {{ + (hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n + }} + {{ + (hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n + }} + @@ -100,17 +105,22 @@

{{ "chooseCipherForPasskeySave" | i18n }}

- {{ "noItemsMatchSearch" | i18n }} - {{ "clearFiltersOrTryAnother" | i18n }} + {{ + (hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n + }} + {{ + (hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n + }} + diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index cf0fd90a8fd..82be95ea0da 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -91,7 +91,6 @@ interface ViewData { export class Fido2Component implements OnInit, OnDestroy { private destroy$ = new Subject(); private message$ = new BehaviorSubject(null); - private hasSearched = false; protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; protected cipher: CipherView; protected ciphers?: CipherView[] = []; @@ -104,6 +103,7 @@ export class Fido2Component implements OnInit, OnDestroy { protected noResultsIcon = Icons.NoResults; protected passkeyAction: PasskeyActionValue = PasskeyActions.Register; protected PasskeyActions = PasskeyActions; + protected hasSearched = false; protected searchText: string; protected searchTypeSearch = false; protected senderTabId?: string; @@ -370,19 +370,30 @@ export class Fido2Component implements OnInit, OnDestroy { return this.equivalentDomains; } + async clearSearch() { + this.searchText = ""; + await this.setDisplayedCiphersToAllDomainMatch(); + } + + protected async setDisplayedCiphersToAllDomainMatch() { + const equivalentDomains = await this.getEquivalentDomains(); + this.displayedCiphers = this.ciphers.filter((cipher) => + cipher.login.matchesUri(this.url, equivalentDomains), + ); + } + protected async search() { - this.hasSearched = await this.searchService.isSearchable(this.searchText); - if (this.hasSearched) { + this.hasSearched = true; + const isSearchable = await this.searchService.isSearchable(this.searchText); + + if (isSearchable) { this.displayedCiphers = await this.searchService.searchCiphers( this.searchText, null, this.ciphers, ); } else { - const equivalentDomains = await this.getEquivalentDomains(); - this.displayedCiphers = this.ciphers.filter((cipher) => - cipher.login.matchesUri(this.url, equivalentDomains), - ); + await this.setDisplayedCiphersToAllDomainMatch(); } } From c9de05de95220db60f967432d4fbc798b4d6a0e6 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Mon, 21 Oct 2024 09:50:59 -0400 Subject: [PATCH 57/74] [PM-13675] Adjust browser autofill override instructions conditions and placement in the settings view (#11559) * adjust browser autofill override instructions conditions and placement in the settings view * adjust placement of override instructions in the refresh component for Firefox --- .../popup/settings/autofill-v1.component.html | 17 ++++++++-- .../popup/settings/autofill.component.html | 31 ++++++++++--------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html index ec8aeac37e9..530519e88f1 100644 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html @@ -41,8 +41,19 @@

-
@@ -86,7 +97,7 @@

/>

-

- + {{ "autofillSuggestionsSectionTitle" | i18n }} > {{ "showInlineMenuOnFormFieldsDescAlt" | i18n }} + + {{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }} + + {{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }} + + {{ "autofillSuggestionsSectionTitle" | i18n }} {{ "showInlineMenuOnIconSelectionLabel" | i18n }} - - {{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }} - - {{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }} - - Date: Mon, 21 Oct 2024 16:10:57 +0200 Subject: [PATCH 58/74] [PM-13189] Send copy updates (#11619) * Add more descriptive header text * Update hint under optional password field --------- Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 10 ++++++++-- .../add-edit/send-add-edit.component.ts | 19 +++++++++++++------ .../options/send-options.component.html | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 290663f4347..026d3b535ca 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2464,8 +2464,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -4494,9 +4494,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index 20b472f97f3..407a4d414a5 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -174,18 +174,25 @@ export class SendAddEditComponent { ) .subscribe((config) => { this.config = config; - this.headerText = this.getHeaderText(config.mode); + this.headerText = this.getHeaderText(config.mode, config.sendType); }); } /** - * Gets the header text based on the mode. + * Gets the header text based on the mode and type. * @param mode The mode of the send form. + * @param type The type of the send * @returns The header text. */ - private getHeaderText(mode: SendFormMode) { - return this.i18nService.t( - mode === "edit" || mode === "partial-edit" ? "editSend" : "createSend", - ); + private getHeaderText(mode: SendFormMode, type: SendType) { + const headerKey = + mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; + + switch (type) { + case SendType.Text: + return this.i18nService.t(headerKey, this.i18nService.t("textSend")); + case SendType.File: + return this.i18nService.t(headerKey, this.i18nService.t("fileSend")); + } } } diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index 4da3466f708..265016ad1b1 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -41,7 +41,7 @@

{{ "additionalOptions" | i18n }}

[appCopyClick]="sendOptionsForm.get('password').value" showToast > - {{ "sendPasswordDescV2" | i18n }} + {{ "sendPasswordDescV3" | i18n }} Date: Mon, 21 Oct 2024 17:56:50 +0200 Subject: [PATCH 59/74] =?UTF-8?q?[PM-12527]=20[Defect]=20Remove=20unnecess?= =?UTF-8?q?ary=20"General=20Information"=20Header=20in=20Organizat?= =?UTF-8?q?=E2=80=A6=20(#11198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/billing/organizations/organization-plans.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index e6e2610d67c..16c5259e8ac 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -46,7 +46,7 @@ *ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans" class="tw-pt-6" > - + Date: Mon, 21 Oct 2024 12:07:28 -0400 Subject: [PATCH 60/74] [PM-13402] Adding the service to get member cipher details (#11544) * Adding the service to get member cipher details * Moving member cipher details to bitwarden license * Adding documentation to the api call --- .../reports/access-intelligence/index.ts | 0 .../member-cipher-details.response.ts | 16 +++ .../member-cipher-details-api.service.spec.ts | 105 ++++++++++++++++++ .../member-cipher-details-api.service.ts | 27 +++++ 4 files changed, 148 insertions(+) create mode 100644 bitwarden_license/bit-common/src/tools/reports/access-intelligence/index.ts create mode 100644 bitwarden_license/bit-common/src/tools/reports/access-intelligence/response/member-cipher-details.response.ts create mode 100644 bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.spec.ts create mode 100644 bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.ts diff --git a/bitwarden_license/bit-common/src/tools/reports/access-intelligence/index.ts b/bitwarden_license/bit-common/src/tools/reports/access-intelligence/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bitwarden_license/bit-common/src/tools/reports/access-intelligence/response/member-cipher-details.response.ts b/bitwarden_license/bit-common/src/tools/reports/access-intelligence/response/member-cipher-details.response.ts new file mode 100644 index 00000000000..fcf5ada4b2c --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/access-intelligence/response/member-cipher-details.response.ts @@ -0,0 +1,16 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class MemberCipherDetailsResponse extends BaseResponse { + userName: string; + email: string; + useKeyConnector: boolean; + cipherIds: string[] = []; + + constructor(response: any) { + super(response); + this.userName = this.getResponseProperty("UserName"); + this.email = this.getResponseProperty("Email"); + this.useKeyConnector = this.getResponseProperty("UseKeyConnector"); + this.cipherIds = this.getResponseProperty("CipherIds"); + } +} diff --git a/bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.spec.ts new file mode 100644 index 00000000000..b71abe075e6 --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.spec.ts @@ -0,0 +1,105 @@ +import { mock } from "jest-mock-extended"; + +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", + ], + }, + ], +}; + +describe("Member Cipher Details API Service", () => { + let memberCipherDetailsApiService: MemberCipherDetailsApiService; + + const apiService = mock(); + + beforeEach(() => { + memberCipherDetailsApiService = new MemberCipherDetailsApiService(apiService); + jest.resetAllMocks(); + }); + + it("instantiates", () => { + expect(memberCipherDetailsApiService).not.toBeFalsy(); + }); + + it("getMemberCipherDetails retrieves data", async () => { + apiService.send.mockResolvedValue(mockMemberCipherDetails); + + const orgId = "1234"; + const result = await memberCipherDetailsApiService.getMemberCipherDetails(orgId); + expect(result).not.toBeNull(); + expect(result).toHaveLength(6); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + "/reports/member-cipher-details/" + orgId, + null, + true, + true, + ); + }); +}); diff --git a/bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.ts b/bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.ts new file mode 100644 index 00000000000..9351ac87776 --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/access-intelligence/services/member-cipher-details-api.service.ts @@ -0,0 +1,27 @@ +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"; + +export class MemberCipherDetailsApiService { + constructor(private apiService: ApiService) {} + + /** + * Returns a list of organization members with their assigned + * cipherIds + * @param orgId OrganizationId to get member cipher details for + * @returns List of organization members and assigned cipherIds + */ + async getMemberCipherDetails(orgId: string): Promise { + const response = await this.apiService.send( + "GET", + "/reports/member-cipher-details/" + orgId, + null, + true, + true, + ); + + const listResponse = new ListResponse(response, MemberCipherDetailsResponse); + return listResponse.data.map((r) => new MemberCipherDetailsResponse(r)); + } +} From 77c50860a9d6a2ece901d97488985208f0d6a2e8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:30:25 -0400 Subject: [PATCH 61/74] [PM-12290] Show self-host options for CB MSP managed orgs (#11465) * (No Logic) organization-subscription-cloud.component.ts cleanup * Show only selfhost options for org owners and provider admins for managed orgs * Fix messages.json issue --- .../icons/manage-billing.icon.ts | 25 ---- .../icons/subscription-hidden.icon.ts | 24 ++++ ...nization-subscription-cloud.component.html | 111 +++++++++-------- ...ganization-subscription-cloud.component.ts | 112 +++++++----------- apps/web/src/locales/en/messages.json | 10 ++ .../organization-billing-metadata.response.ts | 2 + 6 files changed, 131 insertions(+), 153 deletions(-) delete mode 100644 apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts create mode 100644 apps/web/src/app/billing/organizations/icons/subscription-hidden.icon.ts diff --git a/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts b/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts deleted file mode 100644 index 6f583bf2e81..00000000000 --- a/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { svgIcon } from "@bitwarden/components"; - -export const ManageBilling = svgIcon` - - - - - - - - - - - - - - - - - - - - - - `; diff --git a/apps/web/src/app/billing/organizations/icons/subscription-hidden.icon.ts b/apps/web/src/app/billing/organizations/icons/subscription-hidden.icon.ts new file mode 100644 index 00000000000..82490e82a1d --- /dev/null +++ b/apps/web/src/app/billing/organizations/icons/subscription-hidden.icon.ts @@ -0,0 +1,24 @@ +import { Icon, svgIcon } from "@bitwarden/components"; + +export const SubscriptionHiddenIcon: Icon = svgIcon` + + + + + + + + + + + + + + + + + + + + +`; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 643eeb93bad..0a83b2a56ce 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -1,17 +1,12 @@ - - - - {{ "loading" | i18n }} - - - + + + {{ "loading" | i18n }} + - + + {{ "secretsManager" | i18n }} > - -

- {{ "selfHostingTitle" | i18n }} -

-

- {{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }} - - -

-
- - -
+

{{ "additionalOptions" | i18n }}

@@ -302,13 +262,50 @@

{{ "additionalOptions" | i18n }}

- -
- - {{ - "manageBillingFromProviderPortalMessage" | i18n - }} -
-
+ + + +

{{ "manageSubscription" | i18n }}

+

+ {{ "manageSubscriptionFromThe" | i18n }} + {{ + "providerPortal" | i18n + }}. +

+ +

+ {{ "billingManagedByProvider" | i18n: userOrg.providerName }}. + {{ "billingContactProviderForAssistance" | i18n }}. +

+
+ +
+ +
+ +

{{ "billingManagedByProvider" | i18n: userOrg.providerName }}

+

{{ "billingContactProviderForAssistance" | i18n }}

+
+
+
+
+ + + +

+ {{ "selfHostingTitleProper" | i18n }} +

+

+ {{ "toHostBitwardenOnYourOwnServer" | i18n }} +

+
+ + +
+
+
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 7a66faa0a43..e604140e569 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 @@ -5,9 +5,9 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.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 { OrganizationApiKeyType, ProviderStatusType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response"; @@ -15,7 +15,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { @@ -34,7 +33,7 @@ import { import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; import { ChangePlanDialogResultType, openChangePlanDialog } from "./change-plan-dialog.component"; import { DownloadLicenceDialogComponent } from "./download-license.component"; -import { ManageBilling } from "./icons/manage-billing.icon"; +import { SubscriptionHiddenIcon } from "./icons/subscription-hidden.icon"; import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component"; @Component({ @@ -50,19 +49,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; - firstLoaded = false; - loading: boolean; + loading = true; locale: string; showUpdatedSubscriptionStatusSection$: Observable; - manageBillingFromProviderPortal = ManageBilling; - isManagedByConsolidatedBillingMSP = false; enableTimeThreshold: boolean; preSelectedProductTier: ProductTierType = ProductTierType.Free; + showSubscription = true; + showSelfHost = false; + protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - private destroy$ = new Subject(); - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, ); @@ -71,7 +68,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy FeatureFlag.EnableTimeThreshold, ); - protected EnableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( + protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( FeatureFlag.EnableUpgradePasswordManagerSub, ); @@ -79,9 +76,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy FeatureFlag.AC2476_DeprecateStripeSourcesAPI, ); + private destroy$ = new Subject(); + constructor( private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private logService: LogService, private organizationService: OrganizationService, @@ -89,15 +87,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private route: ActivatedRoute, private dialogService: DialogService, private configService: ConfigService, - private providerService: ProviderService, private toastService: ToastService, + private billingApiService: BillingApiServiceAbstraction, ) {} async ngOnInit() { if (this.route.snapshot.queryParamMap.get("upgrade")) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.changePlan(); + await this.changePlan(); const productTierTypeStr = this.route.snapshot.queryParamMap.get("productTierType"); if (productTierTypeStr != null) { const productTier = Number(productTierTypeStr); @@ -112,7 +108,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy concatMap(async (params) => { this.organizationId = params.organizationId; await this.load(); - this.firstLoaded = true; }), takeUntil(this.destroy$), ) @@ -130,21 +125,34 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } async load() { - if (this.loading) { - return; - } this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); - if (this.userOrg.canViewSubscription) { - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - const provider = await this.providerService.get(this.userOrg.providerId); - this.isManagedByConsolidatedBillingMSP = - enableConsolidatedBilling && - this.userOrg.hasProvider && - provider?.providerStatus == ProviderStatusType.Billable; + /* + +--------------------+--------------+----------------------+--------------+ + | User Type | Has Provider | Consolidated Billing | Subscription | + +--------------------+--------------+----------------------+--------------+ + | Organization Owner | False | N/A | Shown | + | Organization Owner | True | N/A | Hidden | + | Provider User | True | False | Shown | + | Provider User | True | True | Hidden | + +--------------------+--------------+----------------------+--------------+ + */ + + const consolidatedBillingEnabled = await firstValueFrom(this.enableConsolidatedBilling$); + this.showSubscription = + (!this.userOrg.hasProvider && this.userOrg.isOwner) || + (this.userOrg.hasProvider && this.userOrg.isProviderUser && !consolidatedBillingEnabled); + + const metadata = await this.billingApiService.getOrganizationBillingMetadata( + this.organizationId, + ); + + this.showSelfHost = metadata.isEligibleForSelfHost; + + if (this.showSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; @@ -277,26 +285,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return this.sub.subscription?.items.some((i) => i.sponsoredSubscriptionItem); } - get canDownloadLicense() { - return ( - (this.sub.planType !== PlanType.Free && this.subscription == null) || - (this.subscription != null && !this.subscription.cancelled) - ); - } - - get canManageBillingSync() { - return ( - this.sub.planType === PlanType.EnterpriseAnnually || - this.sub.planType === PlanType.EnterpriseMonthly || - this.sub.planType === PlanType.EnterpriseAnnually2023 || - this.sub.planType === PlanType.EnterpriseMonthly2023 || - this.sub.planType === PlanType.EnterpriseAnnually2020 || - this.sub.planType === PlanType.EnterpriseMonthly2020 || - this.sub.planType === PlanType.EnterpriseAnnually2019 || - this.sub.planType === PlanType.EnterpriseMonthly2019 - ); - } - get subscriptionDesc() { if (this.sub.planType === PlanType.Free) { return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString()); @@ -353,13 +341,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy ); } - shownSelfHost(): boolean { - return ( - this.sub?.plan.productTier !== ProductTierType.Teams && - this.sub?.plan.productTier !== ProductTierType.Free - ); - } - cancelSubscription = async () => { const reference = openOffboardingSurvey(this.dialogService, { data: { @@ -399,9 +380,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy title: null, message: this.i18nService.t("reinstated"), }); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); + await this.load(); } catch (e) { this.logService.error(e); } @@ -409,7 +388,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy async changePlan() { const EnableUpgradePasswordManagerSub = await firstValueFrom( - this.EnableUpgradePasswordManagerSub$, + this.enableUpgradePasswordManagerSub$, ); if (EnableUpgradePasswordManagerSub) { const reference = openChangePlanDialog(this.dialogService, { @@ -458,24 +437,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy }); await firstValueFrom(dialogRef.closed); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } - - closeDownloadLicense() { - this.showDownloadLicense = false; + await this.load(); } - subscriptionAdjusted() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); + async subscriptionAdjusted() { + await this.load(); } calculateTotalAppliedDiscount(total: number) { - const discountedTotal = total / (1 - this.customerDiscount?.percentOff / 100); - return discountedTotal; + return total / (1 - this.customerDiscount?.percentOff / 100); } adjustStorage = (add: boolean) => { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index d9be5d769bd..c50775efa6e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9457,5 +9457,15 @@ }, "permanentlyDeleteAttachmentConfirmation": { "message": "Are you sure you want to permanently delete this attachment?" + }, + "manageSubscriptionFromThe": { + "message": "Manage subscription from the", + "description": "This represents the beginning of a sentence. The full sentence will be 'Manage subscription from the Provider Portal', but 'Provider Portal' will be a link and thus cannot be included in the translation file." + }, + "toHostBitwardenOnYourOwnServer": { + "message": "To host Bitwarden on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up automatic sync in your self-hosted organization." + }, + "selfHostingTitleProper": { + "message": "Self-Hosting" } } diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts index 33d7907fa88..4831d290698 100644 --- a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -1,10 +1,12 @@ import { BaseResponse } from "../../../models/response/base.response"; export class OrganizationBillingMetadataResponse extends BaseResponse { + isEligibleForSelfHost: boolean; isOnSecretsManagerStandalone: boolean; constructor(response: any) { super(response); + this.isEligibleForSelfHost = this.getResponseProperty("IsEligibleForSelfHost"); this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); } } From 4b9fbfc8326d7f1c714a7f4a0738484df06f2bad Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:45:07 -0700 Subject: [PATCH 62/74] [PM-13769] - fix routing for send created page (#11629) * fix routing for send created page * fix test --------- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- .../popup/send-v2/add-edit/send-add-edit.component.ts | 4 ++-- .../send-v2/send-created/send-created.component.html | 4 ++-- .../send-created/send-created.component.spec.ts | 10 +++++++--- .../send-v2/send-created/send-created.component.ts | 8 +++++++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index 407a4d414a5..585f6067e3d 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -114,8 +114,8 @@ export class SendAddEditComponent { /** * Handles the event when the send is updated. */ - onSendUpdated(send: SendView) { - this.location.back(); + async onSendUpdated(_: SendView) { + await this.router.navigate(["/tabs/send"]); } deleteSend = async () => { diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index 7c65cbeb17d..cdb514b8047 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -4,7 +4,7 @@ slot="header" [pageTitle]="'createdSend' | i18n" showBackButton - [backAction]="close.bind(this)" + [backAction]="goToEditSend.bind(this)" > @@ -27,7 +27,7 @@

{{ "createdSendSuccessfully" | i18n }}

- diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index 24186ad4275..fdf147b360f 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -11,6 +11,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { ButtonModule, I18nMockService, IconModule, ToastService } from "@bitwarden/components"; @@ -50,6 +51,7 @@ describe("SendCreatedComponent", () => { sendView = { id: sendId, deletionDate: new Date(), + type: SendType.Text, accessId: "abc", urlB64Key: "123", } as SendView; @@ -129,9 +131,11 @@ describe("SendCreatedComponent", () => { expect(component["hoursAvailable"]).toBe(0); }); - it("should navigate back to send list on close", async () => { - await component.close(); - expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]); + it("should navigate back to the edit send form on close", async () => { + await component.goToEditSend(); + expect(router.navigate).toHaveBeenCalledWith(["/edit-send"], { + queryParams: { sendId: "test-send-id", type: SendType.Text }, + }); }); describe("getHoursAvailable", () => { diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index 88475d7dad9..98b09d380e4 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -77,7 +77,13 @@ export class SendCreatedComponent { return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60))); } - async close() { + async goToEditSend() { + await this.router.navigate([`/edit-send`], { + queryParams: { sendId: this.send.id, type: this.send.type }, + }); + } + + async goBack() { await this.router.navigate(["/tabs/send"]); } From 116d2166c32926cd9fbd5785abb4f772243ae522 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:05:15 -0500 Subject: [PATCH 63/74] remove slideIn animation as it doesn't support the "show animations" setting (#11591) --- .../vault-generator-dialog.component.html | 2 +- .../vault-generator-dialog.component.ts | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html index 7652b8ab0bf..72aaeea493d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html @@ -1,4 +1,4 @@ - + Date: Mon, 21 Oct 2024 11:50:50 -0700 Subject: [PATCH 64/74] fix voiceover on send created screen (#11628) --- .../popup/send-v2/send-created/send-created.component.html | 4 +++- .../components/send-details/send-details.component.html | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index cdb514b8047..af3abbf5427 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -15,7 +15,9 @@ class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5" > -

{{ "createdSendSuccessfully" | i18n }}

+

+ {{ "createdSendSuccessfully" | i18n }} +

{{ formatExpirationDate() }}

diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html index d4c253303bd..93db4df3187 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html @@ -6,7 +6,7 @@

{{ "sendDetails" | i18n }}

{{ "name" | i18n }} - + Date: Mon, 21 Oct 2024 13:36:27 -0700 Subject: [PATCH 65/74] [PM-13809] - add remove password button (#11641) * add remove password button * adjust comment * use bitAction directive --- .../options/send-options.component.html | 59 +++++++++++-------- .../options/send-options.component.ts | 49 ++++++++++++++- 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index 265016ad1b1..98da24b5188 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -12,33 +12,44 @@

{{ "additionalOptions" | i18n }}

> - {{ "password" | i18n }} - {{ "newPassword" | i18n }} + {{ "password" | i18n }} + + + + + - - {{ "sendPasswordDescV3" | i18n }} diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts index c8122dd4315..48ab78465c1 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts @@ -7,14 +7,20 @@ import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { + AsyncActionsModule, + ButtonModule, CardComponent, CheckboxModule, + DialogService, FormFieldModule, IconButtonModule, SectionComponent, SectionHeaderComponent, + ToastService, TypographyModule, } from "@bitwarden/components"; import { CredentialGeneratorService, Generators } from "@bitwarden/generator-core"; @@ -27,6 +33,8 @@ import { SendFormContainer } from "../../send-form-container"; templateUrl: "./send-options.component.html", standalone: true, imports: [ + AsyncActionsModule, + ButtonModule, CardComponent, CheckboxModule, CommonModule, @@ -53,7 +61,7 @@ export class SendOptionsComponent implements OnInit { hideEmail: [false as boolean], }); - get shouldShowNewPassword(): boolean { + get hasPassword(): boolean { return this.originalSendView && this.originalSendView.password !== null; } @@ -71,8 +79,12 @@ export class SendOptionsComponent implements OnInit { constructor( private sendFormContainer: SendFormContainer, + private dialogService: DialogService, + private sendApiService: SendApiService, private formBuilder: FormBuilder, private policyService: PolicyService, + private i18nService: I18nService, + private toastService: ToastService, private generatorService: CredentialGeneratorService, ) { this.sendFormContainer.registerChildForm("sendOptionsForm", this.sendOptionsForm); @@ -110,16 +122,49 @@ export class SendOptionsComponent implements OnInit { }); }; + removePassword = async () => { + if (!this.originalSendView || !this.originalSendView.password) { + return; + } + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "removePassword" }, + content: { key: "removePasswordConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + await this.sendApiService.removePassword(this.originalSendView.id); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedPassword"), + }); + + this.originalSendView.password = null; + this.sendOptionsForm.patchValue({ + password: null, + }); + this.sendOptionsForm.get("password")?.enable(); + }; + ngOnInit() { if (this.sendFormContainer.originalSendView) { this.sendOptionsForm.patchValue({ maxAccessCount: this.sendFormContainer.originalSendView.maxAccessCount, accessCount: this.sendFormContainer.originalSendView.accessCount, - password: null, + password: this.hasPassword ? "************" : null, // 12 masked characters as a placeholder hideEmail: this.sendFormContainer.originalSendView.hideEmail, notes: this.sendFormContainer.originalSendView.notes, }); } + if (this.hasPassword) { + this.sendOptionsForm.get("password")?.disable(); + } + if (!this.config.areSendsAllowed) { this.sendOptionsForm.disable(); } From 79cdf3bf5031264340d4c263547d1092cbb8c2d1 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:53:30 +0000 Subject: [PATCH 66/74] Bumped client version(s) (#11648) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index deb13794a54..00e09536616 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.10.2", + "version": "2024.10.3", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index c679767699a..184ab0c92de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -248,7 +248,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.10.2" + "version": "2024.10.3" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From c16d1e0e74ef46c8020008707f9d29f812ac3c74 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:52:07 -0400 Subject: [PATCH 67/74] AnonLayoutWrapperComponents - Add reset support for null values (#11651) --- ...extension-anon-layout-wrapper.component.ts | 24 +++++++++++-------- .../anon-layout-wrapper.component.ts | 24 +++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 9d7644878d0..db85b28fa64 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -131,31 +131,35 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { return; } - if (data.pageTitle) { - this.pageTitle = this.handleStringOrTranslation(data.pageTitle); + // Null emissions are used to reset the page data as all fields are optional. + + if (data.pageTitle !== undefined) { + this.pageTitle = + data.pageTitle !== null ? this.handleStringOrTranslation(data.pageTitle) : null; } - if (data.pageSubtitle) { - this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle); + if (data.pageSubtitle !== undefined) { + this.pageSubtitle = + data.pageSubtitle !== null ? this.handleStringOrTranslation(data.pageSubtitle) : null; } - if (data.pageIcon) { - this.pageIcon = data.pageIcon; + if (data.pageIcon !== undefined) { + this.pageIcon = data.pageIcon !== null ? data.pageIcon : null; } - if (data.showReadonlyHostname != null) { + if (data.showReadonlyHostname !== undefined) { this.showReadonlyHostname = data.showReadonlyHostname; } - if (data.showAcctSwitcher != null) { + if (data.showAcctSwitcher !== undefined) { this.showAcctSwitcher = data.showAcctSwitcher; } - if (data.showBackButton != null) { + if (data.showBackButton !== undefined) { this.showBackButton = data.showBackButton; } - if (data.showLogo != null) { + if (data.showLogo !== undefined) { this.showLogo = data.showLogo; } } diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts index 27446335740..f805da0700a 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts @@ -14,17 +14,17 @@ export interface AnonLayoutWrapperData { * If a string is provided, it will be presented as is (ex: Organization name) * If a Translation object (supports placeholders) is provided, it will be translated */ - pageTitle?: string | Translation; + pageTitle?: string | Translation | null; /** * The optional subtitle of the page. * If a string is provided, it will be presented as is (ex: user's email) * If a Translation object (supports placeholders) is provided, it will be translated */ - pageSubtitle?: string | Translation; + pageSubtitle?: string | Translation | null; /** * The optional icon to display on the page. */ - pageIcon?: Icon; + pageIcon?: Icon | null; /** * Optional flag to either show the optional environment selector (false) or just a readonly hostname (true). */ @@ -114,19 +114,23 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { return; } - if (data.pageTitle) { - this.pageTitle = this.handleStringOrTranslation(data.pageTitle); + // Null emissions are used to reset the page data as all fields are optional. + + if (data.pageTitle !== undefined) { + this.pageTitle = + data.pageTitle !== null ? this.handleStringOrTranslation(data.pageTitle) : null; } - if (data.pageSubtitle) { - this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle); + if (data.pageSubtitle !== undefined) { + this.pageSubtitle = + data.pageSubtitle !== null ? this.handleStringOrTranslation(data.pageSubtitle) : null; } - if (data.pageIcon) { - this.pageIcon = data.pageIcon; + if (data.pageIcon !== undefined) { + this.pageIcon = data.pageIcon !== null ? data.pageIcon : null; } - if (data.showReadonlyHostname != null) { + if (data.showReadonlyHostname !== undefined) { this.showReadonlyHostname = data.showReadonlyHostname; } From 9a1879b96c4943563fbe5627202dd149b23be1aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:07:45 +0200 Subject: [PATCH 68/74] [deps] Tools: Update @types/papaparse to v5.3.15 (#11645) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 184ab0c92de..05512349b51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,7 @@ "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", - "@types/papaparse": "5.3.14", + "@types/papaparse": "5.3.15", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", @@ -9698,9 +9698,9 @@ } }, "node_modules/@types/papaparse": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", - "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz", + "integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 38440adf92f..372da701a86 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", - "@types/papaparse": "5.3.14", + "@types/papaparse": "5.3.15", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", From 470ddf79ab1b2b4be402ebad8bf4fd560dc66fae Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 22 Oct 2024 08:46:45 -0400 Subject: [PATCH 69/74] [PM-12425] Remove FF: AC-2828_provider-portal-members-page (#11241) * Remove FF: AC-2828_provider-portal-members-page * Thomas' feedback: Fix provider layout --- ...tml => bulk-confirm-dialog.component.html} | 0 .../bulk/bulk-confirm-dialog.component.ts | 87 ++++++ .../components/bulk/bulk-confirm.component.ts | 132 --------- ...html => bulk-remove-dialog.component.html} | 0 .../bulk/bulk-remove-dialog.component.ts | 54 ++++ .../components/bulk/bulk-remove.component.ts | 76 ----- .../members/members.component.ts | 8 +- .../organizations/members/members.module.ts | 8 +- .../manage/bulk/bulk-confirm.component.ts | 37 --- .../manage/bulk/bulk-remove.component.ts | 24 -- .../dialogs/bulk-confirm-dialog.component.ts | 2 +- .../dialogs/bulk-remove-dialog.component.ts | 2 +- .../providers/manage/people.component.html | 203 ------------- .../providers/manage/people.component.ts | 267 ------------------ .../providers/providers-layout.component.html | 2 +- .../providers/providers-routing.module.ts | 27 +- .../providers/providers.module.ts | 6 - libs/common/src/enums/feature-flag.enum.ts | 4 +- 18 files changed, 164 insertions(+), 775 deletions(-) rename apps/web/src/app/admin-console/organizations/members/components/bulk/{bulk-confirm.component.html => bulk-confirm-dialog.component.html} (100%) create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts delete mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts rename apps/web/src/app/admin-console/organizations/members/components/bulk/{bulk-remove.component.html => bulk-remove-dialog.component.html} (100%) create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts delete mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-confirm.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-remove.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html similarity index 100% rename from apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html rename to apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts new file mode 100644 index 00000000000..8e6ec1dbc34 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -0,0 +1,87 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; + +import { + OrganizationUserApiService, + OrganizationUserBulkConfirmRequest, + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/admin-console/common"; +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; +import { DialogService } from "@bitwarden/components"; + +import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component"; +import { BulkUserDetails } from "./bulk-status.component"; + +type BulkConfirmDialogParams = { + organizationId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: "bulk-confirm-dialog.component.html", +}) +export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { + organizationId: string; + organizationKey$: Observable; + users: BulkUserDetails[]; + + constructor( + protected cryptoService: CryptoService, + @Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams, + protected encryptService: EncryptService, + private organizationUserApiService: OrganizationUserApiService, + protected i18nService: I18nService, + private stateProvider: StateProvider, + ) { + super(cryptoService, encryptService, i18nService); + + this.organizationId = dialogParams.organizationId; + this.organizationKey$ = this.stateProvider.activeUserId$.pipe( + switchMap((userId) => this.cryptoService.orgKeys$(userId)), + map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]), + takeUntilDestroyed(), + ); + this.users = dialogParams.users; + } + + protected getCryptoKey = async (): Promise => + await firstValueFrom(this.organizationKey$); + + protected getPublicKeys = async (): Promise< + ListResponse + > => + await this.organizationUserApiService.postOrganizationUsersPublicKey( + this.organizationId, + this.filteredUsers.map((user) => user.id), + ); + + protected isAccepted = (user: BulkUserDetails) => + user.status === OrganizationUserStatusType.Accepted; + + protected postConfirmRequest = async ( + userIdsWithKeys: { id: string; key: string }[], + ): Promise> => { + const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys); + return await this.organizationUserApiService.postOrganizationUserBulkConfirm( + this.organizationId, + request, + ); + }; + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open(BulkConfirmDialogComponent, config); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts deleted file mode 100644 index ee506840628..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; -import { Component, Inject, OnInit } from "@angular/core"; - -import { - OrganizationUserApiService, - OrganizationUserBulkConfirmRequest, -} from "@bitwarden/admin-console/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { DialogService } from "@bitwarden/components"; - -import { BulkUserDetails } from "./bulk-status.component"; - -type BulkConfirmDialogData = { - organizationId: string; - users: BulkUserDetails[]; -}; - -@Component({ - selector: "app-bulk-confirm", - templateUrl: "bulk-confirm.component.html", -}) -export class BulkConfirmComponent implements OnInit { - organizationId: string; - users: BulkUserDetails[]; - - excludedUsers: BulkUserDetails[]; - filteredUsers: BulkUserDetails[]; - publicKeys: Map = new Map(); - fingerprints: Map = new Map(); - statuses: Map = new Map(); - - loading = true; - done = false; - error: string; - - constructor( - @Inject(DIALOG_DATA) protected data: BulkConfirmDialogData, - protected cryptoService: CryptoService, - protected encryptService: EncryptService, - protected apiService: ApiService, - private organizationUserApiService: OrganizationUserApiService, - private i18nService: I18nService, - ) { - this.organizationId = data.organizationId; - this.users = data.users; - } - - async ngOnInit() { - this.excludedUsers = this.users.filter((u) => !this.isAccepted(u)); - this.filteredUsers = this.users.filter((u) => this.isAccepted(u)); - - if (this.filteredUsers.length <= 0) { - this.done = true; - } - - const response = await this.getPublicKeys(); - - for (const entry of response.data) { - const publicKey = Utils.fromB64ToArray(entry.key); - const fingerprint = await this.cryptoService.getFingerprint(entry.userId, publicKey); - if (fingerprint != null) { - this.publicKeys.set(entry.id, publicKey); - this.fingerprints.set(entry.id, fingerprint.join("-")); - } - } - - this.loading = false; - } - - async submit() { - this.loading = true; - try { - const key = await this.getCryptoKey(); - const userIdsWithKeys: any[] = []; - for (const user of this.filteredUsers) { - const publicKey = this.publicKeys.get(user.id); - if (publicKey == null) { - continue; - } - const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey); - userIdsWithKeys.push({ - id: user.id, - key: encryptedKey.encryptedString, - }); - } - const response = await this.postConfirmRequest(userIdsWithKeys); - - response.data.forEach((entry) => { - const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkConfirmMessage"); - this.statuses.set(entry.id, error); - }); - - this.done = true; - } catch (e) { - this.error = e.message; - } - this.loading = false; - } - - protected isAccepted(user: BulkUserDetails) { - return user.status === OrganizationUserStatusType.Accepted; - } - - protected async getPublicKeys() { - return await this.organizationUserApiService.postOrganizationUsersPublicKey( - this.organizationId, - this.filteredUsers.map((user) => user.id), - ); - } - - protected getCryptoKey(): Promise { - return this.cryptoService.getOrgKey(this.organizationId); - } - - protected async postConfirmRequest(userIdsWithKeys: any[]) { - const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys); - return await this.organizationUserApiService.postOrganizationUserBulkConfirm( - this.organizationId, - request, - ); - } - - static open(dialogService: DialogService, config: DialogConfig) { - return dialogService.open(BulkConfirmComponent, config); - } -} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html similarity index 100% rename from apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html rename to apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts new file mode 100644 index 00000000000..9ff097debb0 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts @@ -0,0 +1,54 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { + OrganizationUserApiService, + OrganizationUserBulkResponse, +} from "@bitwarden/admin-console/common"; +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; + +import { BaseBulkRemoveComponent } from "./base-bulk-remove.component"; +import { BulkUserDetails } from "./bulk-status.component"; + +type BulkRemoveDialogParams = { + organizationId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: "bulk-remove-dialog.component.html", +}) +export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent { + organizationId: string; + users: BulkUserDetails[]; + + constructor( + @Inject(DIALOG_DATA) protected dialogParams: BulkRemoveDialogParams, + protected i18nService: I18nService, + private organizationUserApiService: OrganizationUserApiService, + ) { + super(i18nService); + this.organizationId = dialogParams.organizationId; + this.users = dialogParams.users; + this.showNoMasterPasswordWarning = this.users.some( + (u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false, + ); + } + + protected deleteUsers = (): Promise> => + this.organizationUserApiService.removeManyOrganizationUsers( + this.organizationId, + this.users.map((user) => user.id), + ); + + protected get removeUsersWarning() { + return this.i18nService.t("removeOrgUsersConfirmation"); + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open(BulkRemoveDialogComponent, config); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts deleted file mode 100644 index 74939238fcc..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; - -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService } from "@bitwarden/components"; - -import { BulkUserDetails } from "./bulk-status.component"; - -type BulkRemoveDialogData = { - organizationId: string; - users: BulkUserDetails[]; -}; - -@Component({ - selector: "app-bulk-remove", - templateUrl: "bulk-remove.component.html", -}) -export class BulkRemoveComponent { - organizationId: string; - users: BulkUserDetails[]; - - statuses: Map = new Map(); - - loading = false; - done = false; - error: string; - showNoMasterPasswordWarning = false; - - constructor( - @Inject(DIALOG_DATA) protected data: BulkRemoveDialogData, - protected apiService: ApiService, - protected i18nService: I18nService, - private organizationUserApiService: OrganizationUserApiService, - ) { - this.organizationId = data.organizationId; - this.users = data.users; - this.showNoMasterPasswordWarning = this.users.some( - (u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false, - ); - } - - submit = async () => { - this.loading = true; - try { - const response = await this.removeUsers(); - - response.data.forEach((entry) => { - const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkRemovedMessage"); - this.statuses.set(entry.id, error); - }); - this.done = true; - } catch (e) { - this.error = e.message; - } - - this.loading = false; - }; - - protected async removeUsers() { - return await this.organizationUserApiService.removeManyOrganizationUsers( - this.organizationId, - this.users.map((user) => user.id), - ); - } - - protected get removeUsersWarning() { - return this.i18nService.t("removeOrgUsersConfirmation"); - } - - static open(dialogService: DialogService, config: DialogConfig) { - return dialogService.open(BulkRemoveComponent, config); - } -} diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 3cc73c84a97..7ee99ff2e34 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -60,9 +60,9 @@ import { GroupService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; import { openEntityEventsDialog } from "../manage/entity-events.component"; -import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component"; +import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; -import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component"; +import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; import { @@ -541,7 +541,7 @@ export class MembersComponent extends BaseMembersComponent return; } - const dialogRef = BulkRemoveComponent.open(this.dialogService, { + const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { data: { organizationId: this.organization.id, users: this.dataSource.getCheckedUsers(), @@ -620,7 +620,7 @@ export class MembersComponent extends BaseMembersComponent return; } - const dialogRef = BulkConfirmComponent.open(this.dialogService, { + const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { data: { organizationId: this.organization.id, users: this.dataSource.getCheckedUsers(), diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index d849b1f1f3c..d7c5a9bf1df 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -7,9 +7,9 @@ import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; -import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component"; +import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; -import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component"; +import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; import { UserDialogModule } from "./components/member-dialog"; @@ -28,9 +28,9 @@ import { MembersComponent } from "./members.component"; PasswordStrengthV2Component, ], declarations: [ - BulkConfirmComponent, + BulkConfirmDialogComponent, BulkEnableSecretsManagerDialogComponent, - BulkRemoveComponent, + BulkRemoveDialogComponent, BulkRestoreRevokeComponent, BulkStatusComponent, MembersComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-confirm.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-confirm.component.ts deleted file mode 100644 index 918673e63f5..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-confirm.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component, Input } from "@angular/core"; - -import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums"; -import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk-confirm.request"; -import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-confirm.component"; -import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; - -/** - * @deprecated Please use the {@link BulkConfirmDialogComponent} instead. - */ -@Component({ - templateUrl: - "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html", -}) -export class BulkConfirmComponent extends OrganizationBulkConfirmComponent { - @Input() providerId: string; - - protected override isAccepted(user: BulkUserDetails) { - return user.status === ProviderUserStatusType.Accepted; - } - - protected override async getPublicKeys() { - const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id)); - return await this.apiService.postProviderUsersPublicKey(this.providerId, request); - } - - protected override getCryptoKey(): Promise { - return this.cryptoService.getProviderKey(this.providerId); - } - - protected override async postConfirmRequest(userIdsWithKeys: any[]) { - const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys); - return await this.apiService.postProviderUserBulkConfirm(this.providerId, request); - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-remove.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-remove.component.ts deleted file mode 100644 index ea3ea9b5967..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/bulk/bulk-remove.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, Input } from "@angular/core"; - -import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; -import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-remove.component"; - -/** - * @deprecated Please use the {@link BulkRemoveDialogComponent} instead. - */ -@Component({ - templateUrl: - "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html", -}) -export class BulkRemoveComponent extends OrganizationBulkRemoveComponent { - @Input() providerId: string; - - async deleteUsers() { - const request = new ProviderUserBulkRequest(this.users.map((user) => user.id)); - return await this.apiService.deleteManyProviderUsers(this.providerId, request); - } - - protected get removeUsersWarning() { - return this.i18nService.t("removeUsersWarning"); - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index 8a04cb6452d..61145efb783 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -27,7 +27,7 @@ type BulkConfirmDialogParams = { @Component({ templateUrl: - "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html", + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html", }) export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { providerId: string; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts index 16e64703700..b5d5274498c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts @@ -17,7 +17,7 @@ type BulkRemoveDialogParams = { @Component({ templateUrl: - "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html", + "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html", }) export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent { providerId: string; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html deleted file mode 100644 index 36bc6543696..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - -
- - - {{ "all" | i18n }} - {{ allCount }} - - - - {{ "invited" | i18n }} - {{ invitedCount }} - - - - {{ "needsConfirmation" | i18n }} - {{ acceptedCount }} - - - - -
- - - - {{ "loading" | i18n }} - - -

{{ "noUsersInList" | i18n }}

- - - {{ "providerUsersNeedConfirmed" | i18n }} - - - - - - - - - - - - -
- - - - - {{ u.email }} - {{ - "invited" | i18n - }} - {{ - "needsConfirmation" | i18n - }} - {{ u.name }} - - - - {{ "userUsingTwoStep" | i18n }} - - - {{ "providerAdmin" | i18n }} - {{ "serviceUser" | i18n }} - - -
-
-
- - - - - diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts deleted file mode 100644 index 9293f8c6eb7..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { lastValueFrom } from "rxjs"; -import { first } from "rxjs/operators"; - -import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; -import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; -import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request"; -import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { BasePeopleComponent } from "@bitwarden/web-vault/app/admin-console/common/base.people.component"; -import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; -import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; - -import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; -import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; -import { UserAddEditComponent } from "./user-add-edit.component"; - -/** - * @deprecated Please use the {@link MembersComponent} instead. - */ -@Component({ - selector: "provider-people", - templateUrl: "people.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class PeopleComponent - extends BasePeopleComponent - implements OnInit -{ - @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; - @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) - groupsModalRef: ViewContainerRef; - @ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true }) - bulkStatusModalRef: ViewContainerRef; - @ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true }) - bulkConfirmModalRef: ViewContainerRef; - @ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true }) - bulkRemoveModalRef: ViewContainerRef; - - userType = ProviderUserType; - userStatusType = ProviderUserStatusType; - status: ProviderUserStatusType = null; - providerId: string; - accessEvents = false; - - constructor( - apiService: ApiService, - private route: ActivatedRoute, - i18nService: I18nService, - modalService: ModalService, - platformUtilsService: PlatformUtilsService, - cryptoService: CryptoService, - private encryptService: EncryptService, - private router: Router, - searchService: SearchService, - validationService: ValidationService, - logService: LogService, - searchPipe: SearchPipe, - userNamePipe: UserNamePipe, - private providerService: ProviderService, - dialogService: DialogService, - organizationManagementPreferencesService: OrganizationManagementPreferencesService, - private configService: ConfigService, - protected toastService: ToastService, - ) { - super( - apiService, - searchService, - i18nService, - platformUtilsService, - cryptoService, - validationService, - modalService, - logService, - searchPipe, - userNamePipe, - dialogService, - organizationManagementPreferencesService, - toastService, - ); - } - - ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.params.subscribe(async (params) => { - this.providerId = params.providerId; - const provider = await this.providerService.get(this.providerId); - - if (!provider.canManageUsers) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["../"], { relativeTo: this.route }); - return; - } - - this.accessEvents = provider.useEvents; - - await this.load(); - - /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - this.searchControl.setValue(qParams.search); - if (qParams.viewEvents != null) { - const user = this.users.filter((u) => u.id === qParams.viewEvents); - if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.events(user[0]); - } - } - }); - }); - } - - getUsers(): Promise> { - return this.apiService.getProviderUsers(this.providerId); - } - - deleteUser(id: string): Promise { - return this.apiService.deleteProviderUser(this.providerId, id); - } - - revokeUser(id: string): Promise { - // Not implemented. - return null; - } - - restoreUser(id: string): Promise { - // Not implemented. - return null; - } - - reinviteUser(id: string): Promise { - return this.apiService.postProviderUserReinvite(this.providerId, id); - } - - async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise { - const providerKey = await this.cryptoService.getProviderKey(this.providerId); - const key = await this.encryptService.rsaEncrypt(providerKey.key, publicKey); - const request = new ProviderUserConfirmRequest(); - request.key = key.encryptedString; - await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); - } - - async edit(user: ProviderUserUserDetailsResponse) { - const [modal] = await this.modalService.openViewRef( - UserAddEditComponent, - this.addEditModalRef, - (comp) => { - comp.name = this.userNamePipe.transform(user); - comp.providerId = this.providerId; - comp.providerUserId = user != null ? user.id : null; - comp.savedUser.subscribe(() => { - modal.close(); - this.load(); - }); - comp.deletedUser.subscribe(() => { - modal.close(); - this.removeUser(user); - }); - }, - ); - } - - async events(user: ProviderUserUserDetailsResponse) { - await openEntityEventsDialog(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - providerId: this.providerId, - entityId: user.id, - showUser: false, - entity: "user", - }, - }); - } - - async bulkRemove() { - if (this.actionPromise != null) { - return; - } - - const [modal] = await this.modalService.openViewRef( - BulkRemoveComponent, - this.bulkRemoveModalRef, - (comp) => { - comp.providerId = this.providerId; - comp.users = this.getCheckedUsers(); - }, - ); - - await modal.onClosedPromise(); - await this.load(); - } - - async bulkReinvite() { - if (this.actionPromise != null) { - return; - } - - const users = this.getCheckedUsers(); - const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited); - - if (filteredUsers.length <= 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("noSelectedUsersApplicable"), - }); - return; - } - - try { - const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id)); - const response = this.apiService.postManyProviderUserReinvite(this.providerId, request); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - - // Bulk Status component open - const dialogRef = BulkStatusComponent.open(this.dialogService, { - data: { - users: users, - filteredUsers: filteredUsers, - request: response, - successfulMessage: this.i18nService.t("bulkReinviteMessage"), - }, - }); - await lastValueFrom(dialogRef.closed); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } - - async bulkConfirm() { - if (this.actionPromise != null) { - return; - } - - const [modal] = await this.modalService.openViewRef( - BulkConfirmComponent, - this.bulkConfirmModalRef, - (comp) => { - comp.providerId = this.providerId; - comp.users = this.getCheckedUsers(); - }, - ); - - await modal.onClosedPromise(); - await this.load(); - } -} 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 5f9b3f66bc5..0536221cafd 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 @@ -13,7 +13,7 @@ route="manage" *ngIf="showManageTab(provider)" > - + provider.canManageUsers), - ], - data: { - titleId: "people", - }, + { + path: "members", + component: MembersComponent, + canActivate: [ + providerPermissionsGuard((provider: Provider) => provider.canManageUsers), + ], + data: { + titleId: "members", }, - }), + }, { path: "events", component: EventsComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 8ed10a2d6e3..b6c7125c48c 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -24,14 +24,11 @@ import { AddOrganizationComponent } from "./clients/add-organization.component"; import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; -import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component"; -import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component"; import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog.component"; import { BulkRemoveDialogComponent } from "./manage/dialogs/bulk-remove-dialog.component"; import { EventsComponent } from "./manage/events.component"; import { MembersComponent } from "./manage/members.component"; -import { PeopleComponent } from "./manage/people.component"; import { UserAddEditComponent } from "./manage/user-add-edit.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersRoutingModule } from "./providers-routing.module"; @@ -58,14 +55,11 @@ import { SetupComponent } from "./setup/setup.component"; AcceptProviderComponent, AccountComponent, AddOrganizationComponent, - BulkConfirmComponent, BulkConfirmDialogComponent, - BulkRemoveComponent, BulkRemoveDialogComponent, ClientsComponent, CreateOrganizationComponent, EventsComponent, - PeopleComponent, MembersComponent, SetupComponent, SetupProviderComponent, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index fd3833d10e3..905d7299489 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -20,7 +20,7 @@ export enum FeatureFlag { EnableTimeThreshold = "PM-5864-dollar-threshold", InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", - AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page", + VaultBulkManagementAction = "vault-bulk-management-action", IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub", @@ -67,7 +67,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableTimeThreshold]: FALSE, [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, - [FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE, + [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE, From 4a30782939012dbed73d9e659cf6461696af0dce Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 22 Oct 2024 15:15:15 +0200 Subject: [PATCH 70/74] [PM-12281] [PM-12301] [PM-12306] [PM-12334] Move delete item permission to Can Manage (#11289) * Added inputs to the view and edit component to disable or remove the delete button when a user does not have manage rights * Refactored editByCipherId to receive cipherview object * Fixed issue where adding an item on the individual vault throws a null reference * Fixed issue where adding an item on the AC vault throws a null reference * Allow delete in unassigned collection * created reusable service to check if a user has delete permission on an item * Registered service * Used authorizationservice on the browser and desktop Only display the delete button when a user has delete permission * Added comments to the service * Passed active collectionId to add edit component renamed constructor parameter * restored input property used by the web * Fixed dependency issue * Fixed dependency issue * Fixed dependency issue * Modified service to cater for org vault * Updated to include new dependency * Updated components to use the observable * Added check on the cli to know if user has rights to delete an item * Renamed abstraction and renamed implementation to include Default Fixed permission issues * Fixed test to reflect changes in implementation * Modified base classes to use new naming Passed new parameters for the canDeleteCipher * Modified base classes to use new naming Made changes from base class * Desktop changes Updated reference naming * cli changes Updated reference naming Passed new parameters for the canDeleteCipher$ * Updated references * browser changes Updated reference naming Passed new parameters for the canDeleteCipher$ * Modified cipher form dialog to take in active collection id used canDeleteCipher$ on the vault item dialog to disable the delete button when user does not have the required permissions * Fix number of arguments issue * Added active collection id * Updated canDeleteCipher$ arguments * Updated to pass the cipher object * Fixed up refrences and comments * Updated dependency * updated check to canEditUnassignedCiphers * Fixed unit tests * Removed activeCollectionId from cipher form * Fixed issue where bulk delete option shows for can edit users * Fix null reference when checking if a cipher belongs to the unassigned collection * Fixed bug where allowedCollection passed is undefined * Modified cipher by adding a isAdminConsoleAction argument to tell when a reuqest comes from the admin console * Passed isAdminConsoleAction as true when request is from the admin console --- .../browser/src/background/main.background.ts | 10 + .../vault-v2/view-v2/view-v2.component.html | 2 +- .../view-v2/view-v2.component.spec.ts | 7 + .../vault-v2/view-v2/view-v2.component.ts | 5 + .../components/vault/add-edit.component.html | 2 +- .../components/vault/add-edit.component.ts | 4 + .../components/vault/vault-items.component.ts | 2 +- .../components/vault/view.component.html | 2 +- .../popup/components/vault/view.component.ts | 18 +- apps/cli/src/oss-serve-configurator.ts | 1 + .../service-container/service-container.ts | 10 + apps/cli/src/vault.program.ts | 1 + apps/cli/src/vault/delete.command.ts | 10 + .../vault/app/vault/add-edit.component.html | 2 +- .../src/vault/app/vault/add-edit.component.ts | 3 + .../src/vault/app/vault/vault.component.html | 2 + .../src/vault/app/vault/view.component.html | 2 +- .../src/vault/app/vault/view.component.ts | 3 + .../emergency-add-edit-cipher.component.ts | 3 + .../vault-item-dialog.component.html | 2 +- .../vault-item-dialog.component.ts | 24 ++- .../vault-cipher-row.component.html | 2 +- .../vault-items/vault-cipher-row.component.ts | 1 + .../vault-items/vault-items.component.html | 8 +- .../vault-items/vault-items.component.ts | 93 +++++--- .../individual-vault/add-edit.component.html | 2 +- .../individual-vault/add-edit.component.ts | 3 + .../vault/individual-vault/vault.component.ts | 23 +- .../individual-vault/view.component.html | 2 +- .../individual-vault/view.component.spec.ts | 7 + .../vault/individual-vault/view.component.ts | 15 ++ .../app/vault/org-vault/add-edit.component.ts | 4 + .../app/vault/org-vault/vault.component.html | 1 + .../app/vault/org-vault/vault.component.ts | 11 +- .../src/services/jslib-services.module.ts | 9 + .../vault/components/add-edit.component.ts | 17 +- .../src/vault/components/view.component.ts | 10 +- .../cipher-authorization.service.spec.ts | 200 ++++++++++++++++++ .../services/cipher-authorization.service.ts | 86 ++++++++ 39 files changed, 551 insertions(+), 58 deletions(-) create mode 100644 libs/common/src/vault/services/cipher-authorization.service.spec.ts create mode 100644 libs/common/src/vault/services/cipher-authorization.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a3dd1c473ae..e5a4087510c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -177,6 +177,10 @@ import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitw import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + CipherAuthorizationService, + DefaultCipherAuthorizationService, +} from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; @@ -369,6 +373,7 @@ export default class MainBackground { themeStateService: DefaultThemeStateService; autoSubmitLoginBackground: AutoSubmitLoginBackground; sdkService: SdkService; + cipherAuthorizationService: CipherAuthorizationService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -1265,6 +1270,11 @@ export default class MainBackground { } this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); + + this.cipherAuthorizationService = new DefaultCipherAuthorizationService( + this.collectionService, + this.organizationService, + ); } async bootstrap() { diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html index a778d6aaea9..c2645f15ea8 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html @@ -28,7 +28,7 @@ -
+
diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 38416c2c39c..ae2cf88fd1f 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -2,7 +2,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, Subject } from "rxjs"; +import { firstValueFrom, Observable, Subject } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; @@ -12,12 +12,13 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs 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 { CipherId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AsyncActionsModule, ButtonModule, @@ -63,6 +64,16 @@ export interface VaultItemDialogParams { * If true, the "edit" button will be disabled in the dialog. */ disableForm?: boolean; + + /** + * The ID of the active collection. This is know the collection filter selected by the user. + */ + activeCollectionId?: CollectionId; + + /** + * If true, the dialog is being opened from the admin console. + */ + isAdminConsoleAction?: boolean; } export enum VaultItemDialogResult { @@ -204,6 +215,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected formConfig: CipherFormConfig = this.params.formConfig; + protected canDeleteCipher$: Observable; + constructor( @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, private dialogRef: DialogRef, @@ -217,6 +230,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private router: Router, private billingAccountProfileStateService: BillingAccountProfileStateService, private premiumUpgradeService: PremiumUpgradePromptService, + private cipherAuthorizationService: CipherAuthorizationService, ) { this.updateTitle(); } @@ -231,6 +245,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.organization = this.formConfig.organizations.find( (o) => o.id === this.cipher.organizationId, ); + + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( + this.cipher, + [this.params.activeCollectionId], + this.params.isAdminConsoleAction, + ); } this.performingInitialLoad = false; diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 2f38d7c70db..286bbbab5ef 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -132,7 +132,7 @@ {{ "restore" | i18n }} - - diff --git a/apps/web/src/app/vault/individual-vault/view.component.spec.ts b/apps/web/src/app/vault/individual-vault/view.component.spec.ts index 0dd58b846d7..d1bfd221175 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.spec.ts @@ -14,6 +14,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component"; @@ -62,6 +63,12 @@ describe("ViewComponent", () => { useValue: mock(), }, { provide: ConfigService, useValue: mock() }, + { + provide: CipherAuthorizationService, + useValue: { + canDeleteCipher$: jest.fn().mockReturnValue(true), + }, + }, ], }).compileComponents(); diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index 99829e8f086..d30c453a4bd 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -1,6 +1,7 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Inject, OnInit } from "@angular/core"; +import { Observable } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -8,10 +9,12 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga 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 { CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AsyncActionsModule, DialogModule, @@ -34,6 +37,11 @@ export interface ViewCipherDialogParams { */ collections?: CollectionView[]; + /** + * Optional collection ID used to know the collection filter selected. + */ + activeCollectionId?: CollectionId; + /** * If true, the edit button will be disabled in the dialog. */ @@ -71,6 +79,8 @@ export class ViewComponent implements OnInit { cipherTypeString: string; organization: Organization; + canDeleteCipher$: Observable; + constructor( @Inject(DIALOG_DATA) public params: ViewCipherDialogParams, private dialogRef: DialogRef, @@ -81,6 +91,7 @@ export class ViewComponent implements OnInit { private cipherService: CipherService, private toastService: ToastService, private organizationService: OrganizationService, + private cipherAuthorizationService: CipherAuthorizationService, ) {} /** @@ -93,6 +104,10 @@ export class ViewComponent implements OnInit { if (this.cipher.organizationId) { this.organization = await this.organizationService.get(this.cipher.organizationId); } + + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ + this.params.activeCollectionId, + ]); } /** diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 9cb5542a7b7..7a4697f5af6 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -21,6 +21,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -57,6 +58,7 @@ export class AddEditComponent extends BaseAddEditComponent { datePipe: DatePipe, configService: ConfigService, billingAccountProfileStateService: BillingAccountProfileStateService, + cipherAuthorizationService: CipherAuthorizationService, ) { super( cipherService, @@ -79,6 +81,7 @@ export class AddEditComponent extends BaseAddEditComponent { datePipe, configService, billingAccountProfileStateService, + cipherAuthorizationService, ); } @@ -90,6 +93,7 @@ export class AddEditComponent extends BaseAddEditComponent { } protected async loadCipher() { + this.isAdminConsoleAction = true; // Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin const firstCipherCheck = await super.loadCipher(); diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 220d6ef490f..0bcdc52eaeb 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -70,6 +70,7 @@ [viewingOrgVault]="true" [addAccessStatus]="addAccessStatus$ | async" [addAccessToggle]="showAddAccessToggle" + [activeCollection]="selectedCollection?.node" > diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 94bb6011dc7..060ff7824d2 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -828,6 +828,7 @@ export class VaultComponent implements OnInit, OnDestroy { comp.organization = this.organization; comp.organizationId = this.organization.id; comp.cipherId = cipher?.id; + comp.collectionId = this.activeFilter.collectionId; comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); this.refresh(); @@ -897,7 +898,12 @@ export class VaultComponent implements OnInit, OnDestroy { cipher.type, ); - await this.openVaultItemDialog("view", cipherFormConfig, cipher); + await this.openVaultItemDialog( + "view", + cipherFormConfig, + cipher, + this.activeFilter.collectionId as CollectionId, + ); } /** @@ -907,6 +913,7 @@ export class VaultComponent implements OnInit, OnDestroy { mode: VaultItemDialogMode, formConfig: CipherFormConfig, cipher?: CipherView, + activeCollectionId?: CollectionId, ) { const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false; // If the form is disabled, force the mode into `view` @@ -915,6 +922,8 @@ export class VaultComponent implements OnInit, OnDestroy { mode: dialogMode, formConfig, disableForm, + activeCollectionId, + isAdminConsoleAction: true, }); const result = await lastValueFrom(this.vaultItemDialogRef.closed); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 6af0fe2f660..e8d29bd69ba 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -242,6 +242,10 @@ import { } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { + CipherAuthorizationService, + DefaultCipherAuthorizationService, +} from "@bitwarden/common/vault/services/cipher-authorization.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; @@ -1340,6 +1344,11 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, ], }), + safeProvider({ + provide: CipherAuthorizationService, + useClass: DefaultCipherAuthorizationService, + deps: [CollectionService, OrganizationServiceAbstraction], + }), ]; @NgModule({ diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 49129a868be..44eaec03a68 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -23,7 +23,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { UserId } from "@bitwarden/common/types/guid"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; @@ -36,6 +36,7 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view" import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -47,6 +48,7 @@ export class AddEditComponent implements OnInit, OnDestroy { @Input() type: CipherType; @Input() collectionIds: string[]; @Input() organizationId: string = null; + @Input() collectionId: string = null; @Output() onSavedCipher = new EventEmitter(); @Output() onDeletedCipher = new EventEmitter(); @Output() onRestoredCipher = new EventEmitter(); @@ -57,6 +59,8 @@ export class AddEditComponent implements OnInit, OnDestroy { @Output() onGeneratePassword = new EventEmitter(); @Output() onGenerateUsername = new EventEmitter(); + canDeleteCipher$: Observable; + editMode = false; cipher: CipherView; folders$: Observable; @@ -83,6 +87,10 @@ export class AddEditComponent implements OnInit, OnDestroy { reprompt = false; canUseReprompt = true; organization: Organization; + /** + * Flag to determine if the action is being performed from the admin console. + */ + isAdminConsoleAction: boolean = false; protected componentName = ""; protected destroy$ = new Subject(); @@ -118,6 +126,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected win: Window, protected datePipe: DatePipe, protected configService: ConfigService, + protected cipherAuthorizationService: CipherAuthorizationService, ) { this.typeOptions = [ { name: i18nService.t("typeLogin"), value: CipherType.Login }, @@ -314,6 +323,12 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.reprompt) { this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value; } + + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( + this.cipher, + [this.collectionId as CollectionId], + this.isAdminConsoleAction, + ); } async submit(): Promise { diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index ac644acf9e4..4c96c10dac3 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -9,7 +9,7 @@ import { OnInit, Output, } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom, map, Observable } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -28,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -37,6 +38,7 @@ import { Launchable } from "@bitwarden/common/vault/interfaces/launchable"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -45,12 +47,14 @@ const BroadcasterSubscriptionId = "ViewComponent"; @Directive() export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; + @Input() collectionId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); @Output() onShareCipher = new EventEmitter(); @Output() onDeletedCipher = new EventEmitter(); @Output() onRestoredCipher = new EventEmitter(); + canDeleteCipher$: Observable; cipher: CipherView; showPassword: boolean; showPasswordCount: boolean; @@ -105,6 +109,7 @@ export class ViewComponent implements OnDestroy, OnInit { protected datePipe: DatePipe, protected accountService: AccountService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private cipherAuthorizationService: CipherAuthorizationService, ) {} ngOnInit() { @@ -144,6 +149,9 @@ export class ViewComponent implements OnDestroy, OnInit { ); this.showPremiumRequiredTotp = this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ + this.collectionId as CollectionId, + ]); if (this.cipher.folderId) { this.folder = await ( diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts new file mode 100644 index 00000000000..3155825d4d0 --- /dev/null +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -0,0 +1,200 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionId } from "@bitwarden/common/types/guid"; + +import { CipherView } from "../models/view/cipher.view"; + +import { + CipherAuthorizationService, + DefaultCipherAuthorizationService, +} from "./cipher-authorization.service"; + +describe("CipherAuthorizationService", () => { + let cipherAuthorizationService: CipherAuthorizationService; + + const mockCollectionService = mock(); + const mockOrganizationService = mock(); + + // Mock factories + const createMockCipher = ( + organizationId: string | null, + collectionIds: string[], + edit: boolean = true, + ) => ({ + organizationId, + collectionIds, + edit, + }); + + const createMockCollection = (id: string, manage: boolean) => ({ + id, + manage, + }); + + const createMockOrganization = ({ + allowAdminAccessToAllCollectionItems = false, + canEditAllCiphers = false, + canEditUnassignedCiphers = false, + } = {}) => ({ + allowAdminAccessToAllCollectionItems, + canEditAllCiphers, + canEditUnassignedCiphers, + }); + + beforeEach(() => { + jest.clearAllMocks(); + cipherAuthorizationService = new DefaultCipherAuthorizationService( + mockCollectionService, + mockOrganizationService, + ); + }); + + describe("canDeleteCipher$", () => { + it("should return true if cipher has no organizationId", (done) => { + const cipher = createMockCipher(null, []) as CipherView; + + cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => { + const cipher = createMockCipher("org1", []) as CipherView; + const organization = createMockOrganization({ canEditUnassignedCiphers: true }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => { + const cipher = createMockCipher("org1", ["col1"]) as CipherView; + const organization = createMockOrganization({ canEditAllCiphers: true }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { + expect(result).toBe(true); + expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1"); + done(); + }); + }); + + it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => { + const cipher = createMockCipher("org1", []) as CipherView; + const organization = createMockOrganization({ canEditUnassignedCiphers: false }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { + expect(result).toBe(false); + done(); + }); + }); + + it("should return true if activeCollectionId is provided and has manage permission", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const activeCollectionId = "col1" as CollectionId; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", true), + createMockCollection("col2", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService + .canDeleteCipher$(cipher, [activeCollectionId]) + .subscribe((result) => { + expect(result).toBe(true); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + ] as CollectionId[]); + done(); + }); + }); + + it("should return false if activeCollectionId is provided and manage permission is not present", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const activeCollectionId = "col1" as CollectionId; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", false), + createMockCollection("col2", true), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService + .canDeleteCipher$(cipher, [activeCollectionId]) + .subscribe((result) => { + expect(result).toBe(false); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + ] as CollectionId[]); + done(); + }); + }); + + it("should return true if any collection has manage permission", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", false), + createMockCollection("col2", true), + createMockCollection("col3", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { + expect(result).toBe(true); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + "col3", + ] as CollectionId[]); + done(); + }); + }); + + it("should return false if no collection has manage permission", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", false), + createMockCollection("col2", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { + expect(result).toBe(false); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + ] as CollectionId[]); + done(); + }); + }); + }); +}); diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts new file mode 100644 index 00000000000..00c7c412d61 --- /dev/null +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -0,0 +1,86 @@ +import { map, Observable, of, switchMap } from "rxjs"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionId } from "@bitwarden/common/types/guid"; + +import { Cipher } from "../models/domain/cipher"; +import { CipherView } from "../models/view/cipher.view"; + +/** + * Represents either a cipher or a cipher view. + */ +type CipherLike = Cipher | CipherView; + +/** + * Service for managing user cipher authorization. + */ +export abstract class CipherAuthorizationService { + /** + * Determines if the user can delete the specified cipher. + * + * @param {CipherLike} cipher - The cipher object to evaluate for deletion permissions. + * @param {CollectionId[]} [allowedCollections] - Optional. The selected collection id from the vault filter. + * @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console. + * + * @returns {Observable} - An observable that emits a boolean value indicating if the user can delete the cipher. + */ + canDeleteCipher$: ( + cipher: CipherLike, + allowedCollections?: CollectionId[], + isAdminConsoleAction?: boolean, + ) => Observable; +} + +/** + * {@link CipherAuthorizationService} + */ +export class DefaultCipherAuthorizationService implements CipherAuthorizationService { + constructor( + private collectionService: CollectionService, + private organizationService: OrganizationService, + ) {} + + /** + * + * {@link CipherAuthorizationService.canDeleteCipher$} + */ + canDeleteCipher$( + cipher: CipherLike, + allowedCollections?: CollectionId[], + isAdminConsoleAction?: boolean, + ): Observable { + if (cipher.organizationId == null) { + return of(true); + } + + return this.organizationService.get$(cipher.organizationId).pipe( + switchMap((organization) => { + if (isAdminConsoleAction) { + // If the user is an admin, they can delete an unassigned cipher + if (!cipher.collectionIds || cipher.collectionIds.length === 0) { + return of(organization?.canEditUnassignedCiphers === true); + } + + if (organization?.canEditAllCiphers) { + return of(true); + } + } + + return this.collectionService + .decryptedCollectionViews$(cipher.collectionIds as CollectionId[]) + .pipe( + map((allCollections) => { + const shouldFilter = allowedCollections?.some(Boolean); + + const collections = shouldFilter + ? allCollections.filter((c) => allowedCollections.includes(c.id as CollectionId)) + : allCollections; + + return collections.some((collection) => collection.manage); + }), + ); + }), + ); + } +} From 023abe2969068a7fd229e70e3a81d76fc4476b4d Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 22 Oct 2024 10:07:22 -0400 Subject: [PATCH 71/74] [PM-11199] added permission labels to ciphers in AC (#11210) * added permission labels to ciphers in AC --- .../vault-cipher-row.component.html | 6 ++- .../vault-items/vault-cipher-row.component.ts | 52 ++++++++++++++++++- .../app/vault/org-vault/vault.component.ts | 1 + 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 286bbbab5ef..5c4de576ead 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -69,7 +69,11 @@ > - + +

+ {{ permissionText }} +

+