mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-22391] display simple dialog when advanced matching strategy selected for login ciphers (#15260)
* PM-22391 WIP * update autofill base desc * fill cog when match uri open * switch to button, populate dialog when option selected * default strategy hint * update match hint string and dialog behavior * clean up naming for callbacks and variables * revert global setting hint — this will be addressed separately * add tests * update copy and remove repeated copy to use quoted string * Update apps/browser/src/_locales/en/messages.json Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> * add translation to web and desktop, make continue and cancel required --------- Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
This commit is contained in:
@@ -4246,6 +4246,26 @@
|
|||||||
"commonImportFormats": {
|
"commonImportFormats": {
|
||||||
"message": "Common formats",
|
"message": "Common formats",
|
||||||
"description": "Label indicating the most common import formats"
|
"description": "Label indicating the most common import formats"
|
||||||
|
},
|
||||||
|
"uriMatchDefaultStrategyHint": {
|
||||||
|
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||||
|
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||||
|
},
|
||||||
|
"regExAdvancedOptionWarning": {
|
||||||
|
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||||
|
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||||
|
},
|
||||||
|
"startsWithAdvancedOptionWarning": {
|
||||||
|
"message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.",
|
||||||
|
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||||
|
},
|
||||||
|
"uriMatchWarningDialogLink": {
|
||||||
|
"message": "More about match detection",
|
||||||
|
"description": "Link to match detection docs on warning dialog for advance match strategy"
|
||||||
|
},
|
||||||
|
"uriAdvancedOption":{
|
||||||
|
"message": "Advanced options",
|
||||||
|
"description": "Advanced option placeholder for uri option component"
|
||||||
},
|
},
|
||||||
"confirmContinueToBrowserSettingsTitle": {
|
"confirmContinueToBrowserSettingsTitle": {
|
||||||
"message": "Continue to browser settings?",
|
"message": "Continue to browser settings?",
|
||||||
|
|||||||
@@ -3534,6 +3534,22 @@
|
|||||||
"commonImportFormats": {
|
"commonImportFormats": {
|
||||||
"message": "Common formats",
|
"message": "Common formats",
|
||||||
"description": "Label indicating the most common import formats"
|
"description": "Label indicating the most common import formats"
|
||||||
|
},
|
||||||
|
"uriMatchDefaultStrategyHint": {
|
||||||
|
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||||
|
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||||
|
},
|
||||||
|
"regExAdvancedOptionWarning": {
|
||||||
|
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||||
|
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||||
|
},
|
||||||
|
"startsWithAdvancedOptionWarning": {
|
||||||
|
"message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.",
|
||||||
|
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||||
|
},
|
||||||
|
"uriMatchWarningDialogLink": {
|
||||||
|
"message": "More about match detection",
|
||||||
|
"description": "Link to match detection docs on warning dialog for advance match strategy"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"message": "Success"
|
"message": "Success"
|
||||||
|
|||||||
@@ -8907,6 +8907,22 @@
|
|||||||
"commonImportFormats": {
|
"commonImportFormats": {
|
||||||
"message": "Common formats",
|
"message": "Common formats",
|
||||||
"description": "Label indicating the most common import formats"
|
"description": "Label indicating the most common import formats"
|
||||||
|
},
|
||||||
|
"uriMatchDefaultStrategyHint": {
|
||||||
|
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||||
|
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||||
|
},
|
||||||
|
"regExAdvancedOptionWarning": {
|
||||||
|
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||||
|
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||||
|
},
|
||||||
|
"startsWithAdvancedOptionWarning": {
|
||||||
|
"message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.",
|
||||||
|
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||||
|
},
|
||||||
|
"uriMatchWarningDialogLink": {
|
||||||
|
"message": "More about match detection",
|
||||||
|
"description": "Link to match detection docs on warning dialog for advance match strategy"
|
||||||
},
|
},
|
||||||
"maintainYourSubscription": {
|
"maintainYourSubscription": {
|
||||||
"message": "To maintain your subscription for $ORG$, ",
|
"message": "To maintain your subscription for $ORG$, ",
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<bit-simple-dialog>
|
||||||
|
<i
|
||||||
|
bitDialogIcon
|
||||||
|
class="bwi tw-text-3xl bwi-exclamation-triangle tw-text-warning"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ "warningCapitalized" | i18n }}
|
||||||
|
</span>
|
||||||
|
<div bitDialogContent>
|
||||||
|
<p>
|
||||||
|
{{ contentKey | i18n }}
|
||||||
|
<br />
|
||||||
|
<button bitLink type="button" linkType="primary" (click)="openLink($event)">
|
||||||
|
{{ "uriMatchWarningDialogLink" | i18n }}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton type="button" buttonType="primary" (click)="onContinue()">
|
||||||
|
{{ "continue" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bitButton type="button" buttonType="secondary" (click)="onCancel()">
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-simple-dialog>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Component, inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import {
|
||||||
|
ButtonLinkDirective,
|
||||||
|
ButtonModule,
|
||||||
|
DialogModule,
|
||||||
|
DialogService,
|
||||||
|
DIALOG_DATA,
|
||||||
|
DialogRef,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
export type AdvancedUriOptionDialogParams = {
|
||||||
|
contentKey: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
onContinue: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "advanced-uri-option-dialog.component.html",
|
||||||
|
imports: [ButtonLinkDirective, ButtonModule, DialogModule, JslibModule],
|
||||||
|
})
|
||||||
|
export class AdvancedUriOptionDialogComponent {
|
||||||
|
constructor(private dialogRef: DialogRef<boolean>) {}
|
||||||
|
|
||||||
|
protected platformUtilsService = inject(PlatformUtilsService);
|
||||||
|
protected params = inject<AdvancedUriOptionDialogParams>(DIALOG_DATA);
|
||||||
|
|
||||||
|
get contentKey(): string {
|
||||||
|
return this.params.contentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.params.onCancel?.();
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onContinue() {
|
||||||
|
this.params.onContinue?.();
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
openLink(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.platformUtilsService.launchUri("https://bitwarden.com/help/uri-match-detection/");
|
||||||
|
}
|
||||||
|
|
||||||
|
static open(
|
||||||
|
dialogService: DialogService,
|
||||||
|
params: AdvancedUriOptionDialogParams,
|
||||||
|
): DialogRef<boolean> {
|
||||||
|
return dialogService.open<boolean>(AdvancedUriOptionDialogComponent, {
|
||||||
|
data: params,
|
||||||
|
disableClose: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<input bitInput formControlName="uri" #uriInput />
|
<input bitInput formControlName="uri" #uriInput />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-cog"
|
[bitIconButton]="showMatchDetection ? 'bwi-cog-f' : 'bwi-cog'"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
[appA11yTitle]="toggleTitle"
|
[appA11yTitle]="toggleTitle"
|
||||||
(click)="toggleMatchDetection()"
|
(click)="toggleMatchDetection()"
|
||||||
@@ -43,8 +43,16 @@
|
|||||||
*ngFor="let o of uriMatchOptions"
|
*ngFor="let o of uriMatchOptions"
|
||||||
[label]="o.label"
|
[label]="o.label"
|
||||||
[value]="o.value"
|
[value]="o.value"
|
||||||
|
[disabled]="o.disabled"
|
||||||
></bit-option>
|
></bit-option>
|
||||||
</bit-select>
|
</bit-select>
|
||||||
|
<bit-hint *ngIf="getMatchHints() as hints">
|
||||||
|
{{ hints[0] | i18n }}
|
||||||
|
<ng-container *ngIf="hints.length > 1">
|
||||||
|
<b>{{ "warningCapitalized" | i18n }}:</b>
|
||||||
|
{{ hints[1] | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { NG_VALUE_ACCESSOR } from "@angular/forms";
|
import { NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { DialogRef, DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { AdvancedUriOptionDialogComponent } from "./advanced-uri-option-dialog.component";
|
||||||
import { UriOptionComponent } from "./uri-option.component";
|
import { UriOptionComponent } from "./uri-option.component";
|
||||||
|
|
||||||
describe("UriOptionComponent", () => {
|
describe("UriOptionComponent", () => {
|
||||||
let component: UriOptionComponent;
|
let component: UriOptionComponent;
|
||||||
let fixture: ComponentFixture<UriOptionComponent>;
|
let fixture: ComponentFixture<UriOptionComponent>;
|
||||||
|
let dialogServiceMock: jest.Mocked<DialogService>;
|
||||||
|
let dialogRefMock: jest.Mocked<DialogRef<boolean>>;
|
||||||
|
|
||||||
const getToggleMatchDetectionBtn = () =>
|
const getToggleMatchDetectionBtn = () =>
|
||||||
fixture.nativeElement.querySelector(
|
fixture.nativeElement.querySelector(
|
||||||
@@ -26,9 +31,19 @@ describe("UriOptionComponent", () => {
|
|||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
dialogServiceMock = {
|
||||||
|
open: jest.fn().mockReturnValue(dialogRefMock),
|
||||||
|
} as unknown as jest.Mocked<DialogService>;
|
||||||
|
|
||||||
|
dialogRefMock = {
|
||||||
|
close: jest.fn(),
|
||||||
|
afterClosed: jest.fn().mockReturnValue(of(true)),
|
||||||
|
} as unknown as jest.Mocked<DialogRef<boolean>>;
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UriOptionComponent],
|
imports: [UriOptionComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
{ provide: DialogService, useValue: dialogServiceMock },
|
||||||
{
|
{
|
||||||
provide: I18nService,
|
provide: I18nService,
|
||||||
useValue: { t: (...keys: string[]) => keys.filter(Boolean).join(" ") },
|
useValue: { t: (...keys: string[]) => keys.filter(Boolean).join(" ") },
|
||||||
@@ -165,4 +180,36 @@ describe("UriOptionComponent", () => {
|
|||||||
expect(component.remove.emit).toHaveBeenCalled();
|
expect(component.remove.emit).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("advanced match strategy dialog", () => {
|
||||||
|
function testDialogAction(action: "onContinue" | "onCancel", expected: number) {
|
||||||
|
const openSpy = jest
|
||||||
|
.spyOn(AdvancedUriOptionDialogComponent, "open")
|
||||||
|
.mockReturnValue(dialogRefMock);
|
||||||
|
|
||||||
|
component["uriForm"].controls.matchDetection.setValue(UriMatchStrategy.Domain);
|
||||||
|
component["uriForm"].controls.matchDetection.setValue(UriMatchStrategy.StartsWith);
|
||||||
|
|
||||||
|
const [, params] = openSpy.mock.calls[0] as [
|
||||||
|
DialogService,
|
||||||
|
{
|
||||||
|
contentKey: string;
|
||||||
|
onContinue: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
params[action]();
|
||||||
|
|
||||||
|
expect(component["uriForm"].value.matchDetection).toBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should apply the advanced match strategy when the user continues", () => {
|
||||||
|
testDialogAction("onContinue", UriMatchStrategy.StartsWith);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should revert to the previous strategy when the user cancels", () => {
|
||||||
|
testDialogAction("onCancel", UriMatchStrategy.Domain);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
NG_VALUE_ACCESSOR,
|
NG_VALUE_ACCESSOR,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
} from "@angular/forms";
|
} from "@angular/forms";
|
||||||
|
import { concatMap, pairwise } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import {
|
import {
|
||||||
@@ -26,12 +27,15 @@ import {
|
|||||||
} from "@bitwarden/common/models/domain/domain-service";
|
} from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import {
|
import {
|
||||||
|
DialogService,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
SelectModule,
|
SelectModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { AdvancedUriOptionDialogComponent } from "./advanced-uri-option-dialog.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "vault-autofill-uri-option",
|
selector: "vault-autofill-uri-option",
|
||||||
templateUrl: "./uri-option.component.html",
|
templateUrl: "./uri-option.component.html",
|
||||||
@@ -65,16 +69,26 @@ export class UriOptionComponent implements ControlValueAccessor {
|
|||||||
matchDetection: [null as UriMatchStrategySetting],
|
matchDetection: [null as UriMatchStrategySetting],
|
||||||
});
|
});
|
||||||
|
|
||||||
protected uriMatchOptions: { label: string; value: UriMatchStrategySetting }[] = [
|
protected uriMatchOptions: {
|
||||||
|
label: string;
|
||||||
|
value: UriMatchStrategySetting;
|
||||||
|
disabled?: boolean;
|
||||||
|
}[] = [
|
||||||
{ label: this.i18nService.t("default"), value: null },
|
{ label: this.i18nService.t("default"), value: null },
|
||||||
{ label: this.i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
{ label: this.i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||||
{ label: this.i18nService.t("host"), value: UriMatchStrategy.Host },
|
{ label: this.i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||||
{ label: this.i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
|
||||||
{ label: this.i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
|
||||||
{ label: this.i18nService.t("exact"), value: UriMatchStrategy.Exact },
|
{ label: this.i18nService.t("exact"), value: UriMatchStrategy.Exact },
|
||||||
{ label: this.i18nService.t("never"), value: UriMatchStrategy.Never },
|
{ label: this.i18nService.t("never"), value: UriMatchStrategy.Never },
|
||||||
|
{ label: this.i18nService.t("uriAdvancedOption"), value: null, disabled: true },
|
||||||
|
{ label: this.i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
||||||
|
{ label: this.i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected advancedOptionWarningMap: Partial<Record<UriMatchStrategySetting, string>> = {
|
||||||
|
[UriMatchStrategy.StartsWith]: "startsWithAdvancedOptionWarning",
|
||||||
|
[UriMatchStrategy.RegularExpression]: "regExAdvancedOptionWarning",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the option can be reordered. If false, the reorder button will be hidden.
|
* Whether the option can be reordered. If false, the reorder button will be hidden.
|
||||||
*/
|
*/
|
||||||
@@ -147,6 +161,7 @@ export class UriOptionComponent implements ControlValueAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private dialogService: DialogService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
@@ -157,6 +172,36 @@ export class UriOptionComponent implements ControlValueAccessor {
|
|||||||
this.uriForm.statusChanges.pipe(takeUntilDestroyed()).subscribe(() => {
|
this.uriForm.statusChanges.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||||
this.onTouched();
|
this.onTouched();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.uriForm.controls.matchDetection.valueChanges
|
||||||
|
.pipe(
|
||||||
|
pairwise(),
|
||||||
|
concatMap(([previous, current]) => this.handleAdvancedMatch(previous, current)),
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleAdvancedMatch(
|
||||||
|
previous: UriMatchStrategySetting,
|
||||||
|
current: UriMatchStrategySetting,
|
||||||
|
) {
|
||||||
|
const valueChange = previous !== current;
|
||||||
|
const isAdvanced =
|
||||||
|
current === UriMatchStrategy.StartsWith || current === UriMatchStrategy.RegularExpression;
|
||||||
|
|
||||||
|
if (!valueChange || !isAdvanced) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AdvancedUriOptionDialogComponent.open(this.dialogService, {
|
||||||
|
contentKey: this.advancedOptionWarningMap[current],
|
||||||
|
onContinue: () => {
|
||||||
|
this.uriForm.controls.matchDetection.setValue(current);
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
this.uriForm.controls.matchDetection.setValue(previous);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
focusInput() {
|
focusInput() {
|
||||||
@@ -193,4 +238,16 @@ export class UriOptionComponent implements ControlValueAccessor {
|
|||||||
setDisabledState?(isDisabled: boolean): void {
|
setDisabledState?(isDisabled: boolean): void {
|
||||||
isDisabled ? this.uriForm.disable() : this.uriForm.enable();
|
isDisabled ? this.uriForm.disable() : this.uriForm.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMatchHints() {
|
||||||
|
const hints = ["uriMatchDefaultStrategyHint"];
|
||||||
|
const strategy = this.uriForm.get("matchDetection")?.value;
|
||||||
|
if (
|
||||||
|
strategy === UriMatchStrategy.StartsWith ||
|
||||||
|
strategy === UriMatchStrategy.RegularExpression
|
||||||
|
) {
|
||||||
|
hints.push(this.advancedOptionWarningMap[strategy]);
|
||||||
|
}
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user