mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
[PM-13115] Allow users to disable extension content script injections by domain (#11826)
* add disabledInteractionsUris state to the domain settings service * add routes and ui for user disabledInteractionsUris state management * use disabled URIs service state as a preemptive conditon to injecting content scripts * move disabled domains navigation button from account security settings to autofill settings * update disabled domain terminology to blocked domain terminology * update copy * handle blocked domains initializing with null value * add dismissable banner to the vault view when the active autofill tab is on the blocked domains list * add autofill blocked domain indicators to autofill suggestions section header * add BlockBrowserInjectionsByDomain feature flag and put feature behind it * update router config to new style * update tests and cleanup * use full-width-notice slot for domain script injection blocked banner * convert thrown error on content script injection block to a warning and early return * simplify and enspeeden state resolution for blockedInteractionsUris * refactor feature flag state fetching and update tests * document domain settings service * remove vault component presentational updates
This commit is contained in:
@@ -2324,6 +2324,9 @@
|
|||||||
"message": "Domains",
|
"message": "Domains",
|
||||||
"description": "A category title describing the concept of web domains"
|
"description": "A category title describing the concept of web domains"
|
||||||
},
|
},
|
||||||
|
"blockedDomains": {
|
||||||
|
"message": "Blocked domains"
|
||||||
|
},
|
||||||
"excludedDomains": {
|
"excludedDomains": {
|
||||||
"message": "Excluded domains"
|
"message": "Excluded domains"
|
||||||
},
|
},
|
||||||
@@ -2333,6 +2336,15 @@
|
|||||||
"excludedDomainsDescAlt": {
|
"excludedDomainsDescAlt": {
|
||||||
"message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect."
|
"message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect."
|
||||||
},
|
},
|
||||||
|
"blockedDomainsDesc": {
|
||||||
|
"message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect."
|
||||||
|
},
|
||||||
|
"autofillBlockedNotice": {
|
||||||
|
"message": "Autofill is blocked for this website. Review or change this in settings."
|
||||||
|
},
|
||||||
|
"autofillBlockedTooltip": {
|
||||||
|
"message": "Autofill is blocked on this website. Review in settings."
|
||||||
|
},
|
||||||
"websiteItemLabel": {
|
"websiteItemLabel": {
|
||||||
"message": "Website $number$ (URI)",
|
"message": "Website $number$ (URI)",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -2351,6 +2363,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"blockedDomainsSavedSuccess": {
|
||||||
|
"message": "Blocked domain changes saved"
|
||||||
|
},
|
||||||
"excludedDomainsSavedSuccess": {
|
"excludedDomainsSavedSuccess": {
|
||||||
"message": "Excluded domain changes saved"
|
"message": "Excluded domain changes saved"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mock, MockProxy, mockReset } from "jest-mock-extended";
|
import { mock, MockProxy, mockReset } from "jest-mock-extended";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
||||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import {
|
import {
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
Region,
|
Region,
|
||||||
@@ -93,6 +94,7 @@ describe("OverlayBackground", () => {
|
|||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
let cipherService: MockProxy<CipherService>;
|
let cipherService: MockProxy<CipherService>;
|
||||||
let autofillService: MockProxy<AutofillService>;
|
let autofillService: MockProxy<AutofillService>;
|
||||||
|
let configService: MockProxy<ConfigService>;
|
||||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let environmentMock$: BehaviorSubject<CloudEnvironment>;
|
let environmentMock$: BehaviorSubject<CloudEnvironment>;
|
||||||
@@ -149,11 +151,13 @@ describe("OverlayBackground", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configService = mock<ConfigService>();
|
||||||
|
configService.getFeatureFlag$.mockImplementation(() => of(true));
|
||||||
accountService = mockAccountServiceWith(mockUserId);
|
accountService = mockAccountServiceWith(mockUserId);
|
||||||
fakeStateProvider = new FakeStateProvider(accountService);
|
fakeStateProvider = new FakeStateProvider(accountService);
|
||||||
showFaviconsMock$ = new BehaviorSubject(true);
|
showFaviconsMock$ = new BehaviorSubject(true);
|
||||||
neverDomainsMock$ = new BehaviorSubject({});
|
neverDomainsMock$ = new BehaviorSubject({});
|
||||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||||
domainSettingsService.showFavicons$ = showFaviconsMock$;
|
domainSettingsService.showFavicons$ = showFaviconsMock$;
|
||||||
domainSettingsService.neverDomains$ = neverDomainsMock$;
|
domainSettingsService.neverDomains$ = neverDomainsMock$;
|
||||||
logService = mock<LogService>();
|
logService = mock<LogService>();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DefaultDomainSettingsService,
|
DefaultDomainSettingsService,
|
||||||
DomainSettingsService,
|
DomainSettingsService,
|
||||||
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import {
|
import {
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
Region,
|
Region,
|
||||||
@@ -61,6 +62,7 @@ describe("OverlayBackground", () => {
|
|||||||
let overlayBackground: LegacyOverlayBackground;
|
let overlayBackground: LegacyOverlayBackground;
|
||||||
const cipherService = mock<CipherService>();
|
const cipherService = mock<CipherService>();
|
||||||
const autofillService = mock<AutofillService>();
|
const autofillService = mock<AutofillService>();
|
||||||
|
let configService: MockProxy<ConfigService>;
|
||||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
|
|
||||||
@@ -92,7 +94,9 @@ describe("OverlayBackground", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
configService = mock<ConfigService>();
|
||||||
|
configService.getFeatureFlag$.mockImplementation(() => of(true));
|
||||||
|
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||||
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||||
authService = mock<AuthService>();
|
authService = mock<AuthService>();
|
||||||
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
||||||
|
|||||||
@@ -255,4 +255,16 @@
|
|||||||
{{ "showIdentitiesCurrentTabDesc" | i18n }}
|
{{ "showIdentitiesCurrentTabDesc" | i18n }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box list" *ngIf="blockBrowserInjectionsByDomainEnabled">
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/blocked-domains"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "blockedDomains" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export class AutofillV1Component implements OnInit {
|
|||||||
protected autoFillOverlayVisibilityOptions: any[];
|
protected autoFillOverlayVisibilityOptions: any[];
|
||||||
protected disablePasswordManagerLink: string;
|
protected disablePasswordManagerLink: string;
|
||||||
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
|
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
|
||||||
|
protected blockBrowserInjectionsByDomainEnabled: boolean = false;
|
||||||
protected showInlineMenuIdentities: boolean = true;
|
protected showInlineMenuIdentities: boolean = true;
|
||||||
protected showInlineMenuCards: boolean = true;
|
protected showInlineMenuCards: boolean = true;
|
||||||
inlineMenuIsEnabled: boolean = false;
|
inlineMenuIsEnabled: boolean = false;
|
||||||
@@ -120,6 +121,10 @@ export class AutofillV1Component implements OnInit {
|
|||||||
FeatureFlag.InlineMenuPositioningImprovements,
|
FeatureFlag.InlineMenuPositioningImprovements,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.BlockBrowserInjectionsByDomain,
|
||||||
|
);
|
||||||
|
|
||||||
this.inlineMenuIsEnabled = this.isInlineMenuEnabled();
|
this.inlineMenuIsEnabled = this.isInlineMenuEnabled();
|
||||||
|
|
||||||
this.showInlineMenuIdentities =
|
this.showInlineMenuIdentities =
|
||||||
|
|||||||
@@ -282,5 +282,11 @@
|
|||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
</bit-section>
|
</bit-section>
|
||||||
|
<bit-section *ngIf="blockBrowserInjectionsByDomainEnabled">
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/blocked-domains">{{ "blockedDomains" | i18n }}</a>
|
||||||
|
<i slot="end" class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</bit-item>
|
||||||
|
</bit-section>
|
||||||
</div>
|
</div>
|
||||||
</popup-page>
|
</popup-page>
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import {
|
|||||||
|
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
@@ -67,7 +66,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
JslibModule,
|
JslibModule,
|
||||||
LinkModule,
|
LinkModule,
|
||||||
PopOutComponent,
|
PopOutComponent,
|
||||||
PopupFooterComponent,
|
|
||||||
PopupHeaderComponent,
|
PopupHeaderComponent,
|
||||||
PopupPageComponent,
|
PopupPageComponent,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
@@ -87,6 +85,7 @@ export class AutofillComponent implements OnInit {
|
|||||||
protected inlineMenuVisibility: InlineMenuVisibilitySetting =
|
protected inlineMenuVisibility: InlineMenuVisibilitySetting =
|
||||||
AutofillOverlayVisibility.OnFieldFocus;
|
AutofillOverlayVisibility.OnFieldFocus;
|
||||||
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
|
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
|
||||||
|
protected blockBrowserInjectionsByDomainEnabled: boolean = false;
|
||||||
protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown;
|
protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown;
|
||||||
protected disablePasswordManagerURI: DisablePasswordManagerUri =
|
protected disablePasswordManagerURI: DisablePasswordManagerUri =
|
||||||
DisablePasswordManagerUris.Unknown;
|
DisablePasswordManagerUris.Unknown;
|
||||||
@@ -164,6 +163,10 @@ export class AutofillComponent implements OnInit {
|
|||||||
FeatureFlag.InlineMenuPositioningImprovements,
|
FeatureFlag.InlineMenuPositioningImprovements,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.BlockBrowserInjectionsByDomain,
|
||||||
|
);
|
||||||
|
|
||||||
this.showInlineMenuIdentities =
|
this.showInlineMenuIdentities =
|
||||||
this.inlineMenuPositioningImprovementsEnabled &&
|
this.inlineMenuPositioningImprovementsEnabled &&
|
||||||
(await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$));
|
(await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$));
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="{{ 'blockedDomains' | i18n }}" showBackButton>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
|
||||||
|
<div class="tw-bg-background-alt">
|
||||||
|
<p>{{ "blockedDomainsDesc" | i18n }}</p>
|
||||||
|
<bit-section *ngIf="!isLoading">
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "domainsTitle" | i18n }}</h2>
|
||||||
|
<span bitTypography="body2" slot="end">{{ blockedDomainsState?.length || 0 }}</span>
|
||||||
|
</bit-section-header>
|
||||||
|
|
||||||
|
<ng-container *ngIf="blockedDomainsState">
|
||||||
|
<bit-item
|
||||||
|
*ngFor="let domain of blockedDomainsState; let i = index; trackBy: trackByFunction"
|
||||||
|
>
|
||||||
|
<bit-item-content>
|
||||||
|
<bit-label *ngIf="i >= fieldsEditThreshold">{{
|
||||||
|
"websiteItemLabel" | i18n: i + 1
|
||||||
|
}}</bit-label>
|
||||||
|
<input
|
||||||
|
*ngIf="i >= fieldsEditThreshold"
|
||||||
|
#uriInput
|
||||||
|
appInputVerbatim
|
||||||
|
bitInput
|
||||||
|
id="excludedDomain{{ i }}"
|
||||||
|
inputmode="url"
|
||||||
|
name="excludedDomain{{ i }}"
|
||||||
|
type="text"
|
||||||
|
(change)="fieldChange()"
|
||||||
|
[(ngModel)]="blockedDomainsState[i]"
|
||||||
|
/>
|
||||||
|
<div id="excludedDomain{{ i }}" *ngIf="i < fieldsEditThreshold">{{ domain }}</div>
|
||||||
|
</bit-item-content>
|
||||||
|
<button
|
||||||
|
*ngIf="i < fieldsEditThreshold"
|
||||||
|
appA11yTitle="{{ 'remove' | i18n }}"
|
||||||
|
bitIconButton="bwi-minus-circle"
|
||||||
|
buttonType="danger"
|
||||||
|
size="small"
|
||||||
|
slot="end"
|
||||||
|
type="button"
|
||||||
|
(click)="removeDomain(i)"
|
||||||
|
></button>
|
||||||
|
</bit-item>
|
||||||
|
</ng-container>
|
||||||
|
<button bitLink class="tw-pt-2" type="button" linkType="primary" (click)="addNewDomain()">
|
||||||
|
<i class="bwi bwi-plus-circle bwi-fw" aria-hidden="true"></i> {{ "addDomain" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-section>
|
||||||
|
</div>
|
||||||
|
<popup-footer slot="footer">
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
[disabled]="dataIsPristine"
|
||||||
|
(click)="saveChanges()"
|
||||||
|
>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</popup-footer>
|
||||||
|
</popup-page>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import {
|
||||||
|
QueryList,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
|
AfterViewInit,
|
||||||
|
ViewChildren,
|
||||||
|
} from "@angular/core";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import {
|
||||||
|
ButtonModule,
|
||||||
|
CardComponent,
|
||||||
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
LinkModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
ToastService,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-blocked-domains",
|
||||||
|
templateUrl: "blocked-domains.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CardComponent,
|
||||||
|
CommonModule,
|
||||||
|
FormFieldModule,
|
||||||
|
FormsModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
LinkModule,
|
||||||
|
PopOutComponent,
|
||||||
|
PopupFooterComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopupPageComponent,
|
||||||
|
RouterModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class BlockedDomainsComponent implements AfterViewInit, OnDestroy {
|
||||||
|
@ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>> =
|
||||||
|
new QueryList();
|
||||||
|
|
||||||
|
dataIsPristine = true;
|
||||||
|
isLoading = false;
|
||||||
|
blockedDomainsState: string[] = [];
|
||||||
|
storedBlockedDomains: string[] = [];
|
||||||
|
// How many fields should be non-editable before editable fields
|
||||||
|
fieldsEditThreshold: number = 0;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngAfterViewInit() {
|
||||||
|
this.domainSettingsService.blockedInteractionsUris$
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((neverDomains: NeverDomains) => this.handleStateUpdate(neverDomains));
|
||||||
|
|
||||||
|
this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => {
|
||||||
|
this.focusNewUriInput(last);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStateUpdate(neverDomains: NeverDomains) {
|
||||||
|
if (neverDomains) {
|
||||||
|
this.storedBlockedDomains = Object.keys(neverDomains);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockedDomainsState = [...this.storedBlockedDomains];
|
||||||
|
|
||||||
|
// Do not allow the first x (pre-existing) fields to be edited
|
||||||
|
this.fieldsEditThreshold = this.storedBlockedDomains.length;
|
||||||
|
|
||||||
|
this.dataIsPristine = true;
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
focusNewUriInput(elementRef: ElementRef) {
|
||||||
|
if (elementRef?.nativeElement) {
|
||||||
|
elementRef.nativeElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addNewDomain() {
|
||||||
|
// add empty field to the Domains list interface
|
||||||
|
this.blockedDomainsState.push("");
|
||||||
|
|
||||||
|
await this.fieldChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeDomain(i: number) {
|
||||||
|
this.blockedDomainsState.splice(i, 1);
|
||||||
|
|
||||||
|
// If a pre-existing field was dropped, lower the edit threshold
|
||||||
|
if (i < this.fieldsEditThreshold) {
|
||||||
|
this.fieldsEditThreshold--;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fieldChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fieldChange() {
|
||||||
|
if (this.dataIsPristine) {
|
||||||
|
this.dataIsPristine = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveChanges() {
|
||||||
|
if (this.dataIsPristine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
const newBlockedDomainsSaveState: NeverDomains = {};
|
||||||
|
const uniqueBlockedDomains = new Set(this.blockedDomainsState);
|
||||||
|
|
||||||
|
for (const uri of uniqueBlockedDomains) {
|
||||||
|
if (uri && uri !== "") {
|
||||||
|
const validatedHost = Utils.getHostname(uri);
|
||||||
|
|
||||||
|
if (!validatedHost) {
|
||||||
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("excludedDomainsInvalidDomain", uri),
|
||||||
|
title: "",
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't reset via `handleStateUpdate` to allow existing input value correction
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newBlockedDomainsSaveState[validatedHost] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingState = new Set(this.storedBlockedDomains);
|
||||||
|
const newState = new Set(Object.keys(newBlockedDomainsSaveState));
|
||||||
|
const stateIsUnchanged =
|
||||||
|
existingState.size === newState.size &&
|
||||||
|
new Set([...existingState, ...newState]).size === existingState.size;
|
||||||
|
|
||||||
|
// The subscriber updates don't trigger if `setNeverDomains` sets an equivalent state
|
||||||
|
if (stateIsUnchanged) {
|
||||||
|
// Reset UI state directly
|
||||||
|
const constructedNeverDomainsState = this.storedBlockedDomains.reduce(
|
||||||
|
(neverDomains: NeverDomains, uri: string) => ({ ...neverDomains, [uri]: null }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
this.handleStateUpdate(constructedNeverDomainsState);
|
||||||
|
} else {
|
||||||
|
await this.domainSettingsService.setBlockedInteractionsUris(newBlockedDomainsSaveState);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("blockedDomainsSavedSuccess"),
|
||||||
|
title: "",
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("unexpectedError"),
|
||||||
|
title: "",
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't reset via `handleStateUpdate` to preserve input values
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByFunction(index: number, _: string) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import {
|
import {
|
||||||
QueryList,
|
QueryList,
|
||||||
@@ -17,7 +15,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
import { NeverDomains } 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import {
|
import {
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
@@ -28,6 +25,7 @@ import {
|
|||||||
LinkModule,
|
LinkModule,
|
||||||
SectionComponent,
|
SectionComponent,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
|
ToastService,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
@@ -62,7 +60,8 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
|
export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
|
||||||
@ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>>;
|
@ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>> =
|
||||||
|
new QueryList();
|
||||||
|
|
||||||
accountSwitcherEnabled = false;
|
accountSwitcherEnabled = false;
|
||||||
dataIsPristine = true;
|
dataIsPristine = true;
|
||||||
@@ -77,7 +76,7 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private toastService: ToastService,
|
||||||
) {
|
) {
|
||||||
this.accountSwitcherEnabled = enableAccountSwitching();
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
}
|
}
|
||||||
@@ -156,11 +155,11 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
|
|||||||
const validatedHost = Utils.getHostname(uri);
|
const validatedHost = Utils.getHostname(uri);
|
||||||
|
|
||||||
if (!validatedHost) {
|
if (!validatedHost) {
|
||||||
this.platformUtilsService.showToast(
|
this.toastService.showToast({
|
||||||
"error",
|
message: this.i18nService.t("excludedDomainsInvalidDomain", uri),
|
||||||
null,
|
title: "",
|
||||||
this.i18nService.t("excludedDomainsInvalidDomain", uri),
|
variant: "error",
|
||||||
);
|
});
|
||||||
|
|
||||||
// Don't reset via `handleStateUpdate` to allow existing input value correction
|
// Don't reset via `handleStateUpdate` to allow existing input value correction
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -182,7 +181,7 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
|
|||||||
if (stateIsUnchanged) {
|
if (stateIsUnchanged) {
|
||||||
// Reset UI state directly
|
// Reset UI state directly
|
||||||
const constructedNeverDomainsState = this.storedExcludedDomains.reduce(
|
const constructedNeverDomainsState = this.storedExcludedDomains.reduce(
|
||||||
(neverDomains, uri) => ({ ...neverDomains, [uri]: null }),
|
(neverDomains: NeverDomains, uri: string) => ({ ...neverDomains, [uri]: null }),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
this.handleStateUpdate(constructedNeverDomainsState);
|
this.handleStateUpdate(constructedNeverDomainsState);
|
||||||
@@ -190,13 +189,17 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
|
|||||||
await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState);
|
await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
this.toastService.showToast({
|
||||||
"success",
|
message: this.i18nService.t("excludedDomainsSavedSuccess"),
|
||||||
null,
|
title: "",
|
||||||
this.i18nService.t("excludedDomainsSavedSuccess"),
|
variant: "success",
|
||||||
);
|
});
|
||||||
} catch {
|
} catch {
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("unexpectedError"),
|
||||||
|
title: "",
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
|
||||||
// Don't reset via `handleStateUpdate` to preserve input values
|
// Don't reset via `handleStateUpdate` to preserve input values
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|||||||
@@ -98,7 +98,13 @@ describe("AutofillService", () => {
|
|||||||
let messageListener: MockProxy<MessageListener>;
|
let messageListener: MockProxy<MessageListener>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
configService = mock<ConfigService>();
|
||||||
|
configService.getFeatureFlag$.mockImplementation(() => of(false));
|
||||||
|
scriptInjectorService = new BrowserScriptInjectorService(
|
||||||
|
domainSettingsService,
|
||||||
|
platformUtilsService,
|
||||||
|
logService,
|
||||||
|
);
|
||||||
inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus);
|
inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus);
|
||||||
showInlineMenuCardsMock$ = new BehaviorSubject(false);
|
showInlineMenuCardsMock$ = new BehaviorSubject(false);
|
||||||
showInlineMenuIdentitiesMock$ = new BehaviorSubject(false);
|
showInlineMenuIdentitiesMock$ = new BehaviorSubject(false);
|
||||||
@@ -106,10 +112,10 @@ describe("AutofillService", () => {
|
|||||||
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$;
|
autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$;
|
||||||
autofillSettingsService.showInlineMenuCards$ = showInlineMenuCardsMock$;
|
autofillSettingsService.showInlineMenuCards$ = showInlineMenuCardsMock$;
|
||||||
autofillSettingsService.showInlineMenuIdentities$ = showInlineMenuIdentitiesMock$;
|
autofillSettingsService.showInlineMenuIdentities$ = showInlineMenuIdentitiesMock$;
|
||||||
|
autofillSettingsService.autofillOnPageLoad$ = of(true);
|
||||||
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||||
authService = mock<AuthService>();
|
authService = mock<AuthService>();
|
||||||
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
||||||
configService = mock<ConfigService>();
|
|
||||||
messageListener = mock<MessageListener>();
|
messageListener = mock<MessageListener>();
|
||||||
enableChangedPasswordPromptMock$ = new BehaviorSubject(true);
|
enableChangedPasswordPromptMock$ = new BehaviorSubject(true);
|
||||||
enableAddedLoginPromptMock$ = new BehaviorSubject(true);
|
enableAddedLoginPromptMock$ = new BehaviorSubject(true);
|
||||||
@@ -132,7 +138,7 @@ describe("AutofillService", () => {
|
|||||||
userNotificationsSettings,
|
userNotificationsSettings,
|
||||||
messageListener,
|
messageListener,
|
||||||
);
|
);
|
||||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||||
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||||
jest.spyOn(BrowserApi, "tabSendMessage");
|
jest.spyOn(BrowserApi, "tabSendMessage");
|
||||||
});
|
});
|
||||||
@@ -385,6 +391,7 @@ describe("AutofillService", () => {
|
|||||||
);
|
);
|
||||||
tabMock = createChromeTabMock();
|
tabMock = createChromeTabMock();
|
||||||
sender = { tab: tabMock, frameId: 1 };
|
sender = { tab: tabMock, frameId: 1 };
|
||||||
|
jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock);
|
||||||
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
||||||
jest
|
jest
|
||||||
.spyOn(autofillService, "getInlineMenuVisibility")
|
.spyOn(autofillService, "getInlineMenuVisibility")
|
||||||
|
|||||||
@@ -695,7 +695,6 @@ export default class MainBackground {
|
|||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
|
|
||||||
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
|
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
|
||||||
this.cipherFileUploadService = new CipherFileUploadService(
|
this.cipherFileUploadService = new CipherFileUploadService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
@@ -809,6 +808,11 @@ export default class MainBackground {
|
|||||||
this.authService,
|
this.authService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.domainSettingsService = new DefaultDomainSettingsService(
|
||||||
|
this.stateProvider,
|
||||||
|
this.configService,
|
||||||
|
);
|
||||||
|
|
||||||
this.themeStateService = new DefaultThemeStateService(
|
this.themeStateService = new DefaultThemeStateService(
|
||||||
this.globalStateProvider,
|
this.globalStateProvider,
|
||||||
this.configService,
|
this.configService,
|
||||||
@@ -957,6 +961,7 @@ export default class MainBackground {
|
|||||||
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
||||||
|
|
||||||
this.scriptInjectorService = new BrowserScriptInjectorService(
|
this.scriptInjectorService = new BrowserScriptInjectorService(
|
||||||
|
this.domainSettingsService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DomainSettingsService,
|
||||||
|
DefaultDomainSettingsService,
|
||||||
|
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import {
|
||||||
|
FakeStateProvider,
|
||||||
|
FakeAccountService,
|
||||||
|
mockAccountServiceWith,
|
||||||
|
} from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { createChromeTabMock } from "../../autofill/spec/autofill-mocks";
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,8 +25,19 @@ import {
|
|||||||
} from "./abstractions/script-injector.service";
|
} from "./abstractions/script-injector.service";
|
||||||
import { BrowserScriptInjectorService } from "./browser-script-injector.service";
|
import { BrowserScriptInjectorService } from "./browser-script-injector.service";
|
||||||
|
|
||||||
|
const mockEquivalentDomains = [
|
||||||
|
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
|
||||||
|
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
|
||||||
|
["example.co.uk", "exampleapp.co.uk"],
|
||||||
|
];
|
||||||
|
|
||||||
describe("ScriptInjectorService", () => {
|
describe("ScriptInjectorService", () => {
|
||||||
const tabId = 1;
|
const tabId = 1;
|
||||||
|
const tabMock = createChromeTabMock({ id: tabId });
|
||||||
|
const mockBlockedURI = new URL(tabMock.url);
|
||||||
|
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
||||||
|
jest.spyOn(BrowserApi, "isManifestVersion");
|
||||||
|
|
||||||
const combinedManifestVersionFile = "content/autofill-init.js";
|
const combinedManifestVersionFile = "content/autofill-init.js";
|
||||||
const mv2SpecificFile = "content/autofill-init-mv2.js";
|
const mv2SpecificFile = "content/autofill-init-mv2.js";
|
||||||
const mv2Details = { file: mv2SpecificFile };
|
const mv2Details = { file: mv2SpecificFile };
|
||||||
@@ -22,14 +47,29 @@ describe("ScriptInjectorService", () => {
|
|||||||
runAt: "document_start",
|
runAt: "document_start",
|
||||||
};
|
};
|
||||||
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||||
|
|
||||||
let scriptInjectorService: BrowserScriptInjectorService;
|
let scriptInjectorService: BrowserScriptInjectorService;
|
||||||
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
|
|
||||||
jest.spyOn(BrowserApi, "isManifestVersion");
|
|
||||||
const platformUtilsService = mock<PlatformUtilsService>();
|
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
|
const platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
|
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
||||||
|
let configService: MockProxy<ConfigService>;
|
||||||
|
let domainSettingsService: DomainSettingsService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock);
|
||||||
|
configService = mock<ConfigService>();
|
||||||
|
configService.getFeatureFlag$.mockImplementation(() => of(false));
|
||||||
|
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||||
|
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||||
|
domainSettingsService.blockedInteractionsUris$ = of(null);
|
||||||
|
scriptInjectorService = new BrowserScriptInjectorService(
|
||||||
|
domainSettingsService,
|
||||||
|
platformUtilsService,
|
||||||
|
logService,
|
||||||
|
);
|
||||||
|
jest.spyOn(scriptInjectorService as any, "buildInjectionDetails");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inject", () => {
|
describe("inject", () => {
|
||||||
@@ -71,6 +111,58 @@ describe("ScriptInjectorService", () => {
|
|||||||
{ world: "ISOLATED" },
|
{ world: "ISOLATED" },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips injecting the script in manifest v3 when the tab domain is a blocked domain", async () => {
|
||||||
|
domainSettingsService.blockedInteractionsUris$ = of({ [mockBlockedURI.host]: null });
|
||||||
|
manifestVersionSpy.mockReturnValue(3);
|
||||||
|
|
||||||
|
await expect(scriptInjectorService["buildInjectionDetails"]).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips injecting the script in manifest v2 when the tab domain is a blocked domain", async () => {
|
||||||
|
domainSettingsService.blockedInteractionsUris$ = of({ [mockBlockedURI.host]: null });
|
||||||
|
manifestVersionSpy.mockReturnValue(2);
|
||||||
|
|
||||||
|
await expect(scriptInjectorService["buildInjectionDetails"]).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects the script in manifest v2 when given combined injection details", async () => {
|
||||||
|
manifestVersionSpy.mockReturnValue(2);
|
||||||
|
|
||||||
|
await scriptInjectorService.inject({
|
||||||
|
tabId,
|
||||||
|
injectDetails: {
|
||||||
|
file: combinedManifestVersionFile,
|
||||||
|
frame: "all_frames",
|
||||||
|
...sharedInjectDetails,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, {
|
||||||
|
...sharedInjectDetails,
|
||||||
|
allFrames: true,
|
||||||
|
file: combinedManifestVersionFile,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects the script in manifest v3 when given combined injection details", async () => {
|
||||||
|
manifestVersionSpy.mockReturnValue(3);
|
||||||
|
|
||||||
|
await scriptInjectorService.inject({
|
||||||
|
tabId,
|
||||||
|
injectDetails: {
|
||||||
|
file: combinedManifestVersionFile,
|
||||||
|
frame: 10,
|
||||||
|
...sharedInjectDetails,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(
|
||||||
|
tabId,
|
||||||
|
{ ...sharedInjectDetails, frameId: 10, file: combinedManifestVersionFile },
|
||||||
|
{ world: "ISOLATED" },
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("injection of mv2 specific details", () => {
|
describe("injection of mv2 specific details", () => {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -12,7 +15,10 @@ import {
|
|||||||
} from "./abstractions/script-injector.service";
|
} from "./abstractions/script-injector.service";
|
||||||
|
|
||||||
export class BrowserScriptInjectorService extends ScriptInjectorService {
|
export class BrowserScriptInjectorService extends ScriptInjectorService {
|
||||||
|
blockedDomains: Set<string> = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly domainSettingsService: DomainSettingsService,
|
||||||
private readonly platformUtilsService: PlatformUtilsService,
|
private readonly platformUtilsService: PlatformUtilsService,
|
||||||
private readonly logService: LogService,
|
private readonly logService: LogService,
|
||||||
) {
|
) {
|
||||||
@@ -32,6 +38,28 @@ export class BrowserScriptInjectorService extends ScriptInjectorService {
|
|||||||
throw new Error("No file specified for script injection");
|
throw new Error("No file specified for script injection");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tab = tabId && (await BrowserApi.getTab(tabId));
|
||||||
|
const tabURL = tab?.url ? new URL(tab.url) : null;
|
||||||
|
|
||||||
|
// Check if the tab URI is on the disabled URIs list
|
||||||
|
let injectionAllowedInTab = true;
|
||||||
|
const blockedDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.blockedInteractionsUris$,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (blockedDomains && tabURL?.hostname) {
|
||||||
|
const blockedDomainsSet = new Set(Object.keys(blockedDomains));
|
||||||
|
|
||||||
|
injectionAllowedInTab = !(tabURL && blockedDomainsSet.has(tabURL.hostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!injectionAllowedInTab) {
|
||||||
|
this.logService.warning(
|
||||||
|
`${injectDetails.file} was not injected because ${tabURL?.hostname || "the tab URI"} is on the user's blocked domains list.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const injectionDetails = this.buildInjectionDetails(injectDetails, file);
|
const injectionDetails = this.buildInjectionDetails(injectDetails, file);
|
||||||
|
|
||||||
if (BrowserApi.isManifestVersion(3)) {
|
if (BrowserApi.isManifestVersion(3)) {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
|
|||||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-domains.component";
|
||||||
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
|
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
|
||||||
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||||
import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component";
|
import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component";
|
||||||
@@ -348,6 +349,12 @@ const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "blocked-domains",
|
||||||
|
component: BlockedDomainsComponent,
|
||||||
|
canActivate: [authGuard],
|
||||||
|
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||||
|
},
|
||||||
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
||||||
path: "excluded-domains",
|
path: "excluded-domains",
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: DomainSettingsService,
|
provide: DomainSettingsService,
|
||||||
useClass: DefaultDomainSettingsService,
|
useClass: DefaultDomainSettingsService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider, ConfigService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AbstractStorageService,
|
provide: AbstractStorageService,
|
||||||
@@ -365,7 +365,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: ScriptInjectorService,
|
provide: ScriptInjectorService,
|
||||||
useClass: BrowserScriptInjectorService,
|
useClass: BrowserScriptInjectorService,
|
||||||
deps: [PlatformUtilsService, LogService],
|
deps: [DomainSettingsService, PlatformUtilsService, LogService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: VaultTimeoutService,
|
provide: VaultTimeoutService,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -23,18 +24,10 @@ import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import
|
|||||||
|
|
||||||
import FilelessImporterBackground from "./fileless-importer.background";
|
import FilelessImporterBackground from "./fileless-importer.background";
|
||||||
|
|
||||||
jest.mock("rxjs", () => {
|
|
||||||
const rxjs = jest.requireActual("rxjs");
|
|
||||||
const { firstValueFrom } = rxjs;
|
|
||||||
return {
|
|
||||||
...rxjs,
|
|
||||||
firstValueFrom: jest.fn(firstValueFrom),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("FilelessImporterBackground ", () => {
|
describe("FilelessImporterBackground ", () => {
|
||||||
let filelessImporterBackground: FilelessImporterBackground;
|
let filelessImporterBackground: FilelessImporterBackground;
|
||||||
const configService = mock<ConfigService>();
|
const configService = mock<ConfigService>();
|
||||||
|
const domainSettingsService = mock<DomainSettingsService>();
|
||||||
const authService = mock<AuthService>();
|
const authService = mock<AuthService>();
|
||||||
const policyService = mock<PolicyService>();
|
const policyService = mock<PolicyService>();
|
||||||
const notificationBackground = mock<NotificationBackground>();
|
const notificationBackground = mock<NotificationBackground>();
|
||||||
@@ -43,9 +36,16 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
const platformUtilsService = mock<PlatformUtilsService>();
|
const platformUtilsService = mock<PlatformUtilsService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
let scriptInjectorService: BrowserScriptInjectorService;
|
let scriptInjectorService: BrowserScriptInjectorService;
|
||||||
|
let tabMock: chrome.tabs.Tab;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
domainSettingsService.blockedInteractionsUris$ = of(null);
|
||||||
|
policyService.policyAppliesToActiveUser$.mockImplementation(() => of(true));
|
||||||
|
scriptInjectorService = new BrowserScriptInjectorService(
|
||||||
|
domainSettingsService,
|
||||||
|
platformUtilsService,
|
||||||
|
logService,
|
||||||
|
);
|
||||||
filelessImporterBackground = new FilelessImporterBackground(
|
filelessImporterBackground = new FilelessImporterBackground(
|
||||||
configService,
|
configService,
|
||||||
authService,
|
authService,
|
||||||
@@ -75,12 +75,13 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter);
|
lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter);
|
||||||
|
tabMock = lpImporterPort.sender.tab;
|
||||||
|
jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock);
|
||||||
manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||||
executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null);
|
executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null);
|
||||||
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
||||||
jest.spyOn(filelessImporterBackground as any, "removeIndividualVault");
|
jest.spyOn(filelessImporterBackground as any, "removeIndividualVault");
|
||||||
(firstValueFrom as jest.Mock).mockResolvedValue(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => {
|
it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => {
|
||||||
@@ -105,8 +106,6 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => {
|
it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => {
|
||||||
(firstValueFrom as jest.Mock).mockResolvedValue(true);
|
|
||||||
|
|
||||||
triggerRuntimeOnConnectEvent(lpImporterPort);
|
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -129,6 +128,8 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("posts a message to the port indicating that the fileless import feature is enabled", async () => {
|
it("posts a message to the port indicating that the fileless import feature is enabled", async () => {
|
||||||
|
policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false));
|
||||||
|
|
||||||
triggerRuntimeOnConnectEvent(lpImporterPort);
|
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => {
|
it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => {
|
||||||
|
policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false));
|
||||||
manifestVersionSpy.mockReturnValue(3);
|
manifestVersionSpy.mockReturnValue(3);
|
||||||
|
|
||||||
triggerRuntimeOnConnectEvent(lpImporterPort);
|
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||||
@@ -152,6 +154,7 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => {
|
it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => {
|
||||||
|
policyService.policyAppliesToActiveUser$.mockImplementationOnce(() => of(false));
|
||||||
manifestVersionSpy.mockReturnValue(2);
|
manifestVersionSpy.mockReturnValue(2);
|
||||||
|
|
||||||
triggerRuntimeOnConnectEvent(lpImporterPort);
|
triggerRuntimeOnConnectEvent(lpImporterPort);
|
||||||
@@ -170,9 +173,10 @@ describe("FilelessImporterBackground ", () => {
|
|||||||
let lpImporterPort: chrome.runtime.Port;
|
let lpImporterPort: chrome.runtime.Port;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
policyService.policyAppliesToActiveUser$.mockImplementation(() => of(false));
|
||||||
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
|
||||||
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
|
||||||
(firstValueFrom as jest.Mock).mockResolvedValue(false);
|
|
||||||
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar));
|
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar));
|
||||||
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter));
|
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter));
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|||||||
@@ -483,7 +483,29 @@ export class ServiceContainer {
|
|||||||
|
|
||||||
this.containerService = new ContainerService(this.keyService, this.encryptService);
|
this.containerService = new ContainerService(this.keyService, this.encryptService);
|
||||||
|
|
||||||
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
|
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
||||||
|
|
||||||
|
this.authService = new AuthService(
|
||||||
|
this.accountService,
|
||||||
|
this.messagingService,
|
||||||
|
this.keyService,
|
||||||
|
this.apiService,
|
||||||
|
this.stateService,
|
||||||
|
this.tokenService,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.configService = new DefaultConfigService(
|
||||||
|
this.configApiService,
|
||||||
|
this.environmentService,
|
||||||
|
this.logService,
|
||||||
|
this.stateProvider,
|
||||||
|
this.authService,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.domainSettingsService = new DefaultDomainSettingsService(
|
||||||
|
this.stateProvider,
|
||||||
|
this.configService,
|
||||||
|
);
|
||||||
|
|
||||||
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
|
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
|
||||||
|
|
||||||
@@ -579,25 +601,6 @@ export class ServiceContainer {
|
|||||||
|
|
||||||
this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService);
|
this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService);
|
||||||
|
|
||||||
this.authService = new AuthService(
|
|
||||||
this.accountService,
|
|
||||||
this.messagingService,
|
|
||||||
this.keyService,
|
|
||||||
this.apiService,
|
|
||||||
this.stateService,
|
|
||||||
this.tokenService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
|
||||||
|
|
||||||
this.configService = new DefaultConfigService(
|
|
||||||
this.configApiService,
|
|
||||||
this.environmentService,
|
|
||||||
this.logService,
|
|
||||||
this.stateProvider,
|
|
||||||
this.authService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||||
this.deviceTrustService = new DeviceTrustService(
|
this.deviceTrustService = new DeviceTrustService(
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
|
|||||||
@@ -463,6 +463,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: CipherFileUploadService,
|
useClass: CipherFileUploadService,
|
||||||
deps: [ApiServiceAbstraction, FileUploadServiceAbstraction],
|
deps: [ApiServiceAbstraction, FileUploadServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: DomainSettingsService,
|
||||||
|
useClass: DefaultDomainSettingsService,
|
||||||
|
deps: [StateProvider, ConfigService],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CipherServiceAbstraction,
|
provide: CipherServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
@@ -1243,11 +1248,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: BadgeSettingsService,
|
useClass: BadgeSettingsService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: DomainSettingsService,
|
|
||||||
useClass: DefaultDomainSettingsService,
|
|
||||||
deps: [StateProvider],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: BiometricStateService,
|
provide: BiometricStateService,
|
||||||
useClass: DefaultBiometricStateService,
|
useClass: DefaultBiometricStateService,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom, of } from "rxjs";
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
|
||||||
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
@@ -8,6 +11,7 @@ import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-se
|
|||||||
|
|
||||||
describe("DefaultDomainSettingsService", () => {
|
describe("DefaultDomainSettingsService", () => {
|
||||||
let domainSettingsService: DomainSettingsService;
|
let domainSettingsService: DomainSettingsService;
|
||||||
|
let configService: MockProxy<ConfigService>;
|
||||||
const mockUserId = Utils.newGuid() as UserId;
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
||||||
@@ -19,10 +23,13 @@ describe("DefaultDomainSettingsService", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
configService = mock<ConfigService>();
|
||||||
|
configService.getFeatureFlag$.mockImplementation(() => of(false));
|
||||||
|
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||||
|
|
||||||
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
|
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
|
||||||
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||||
|
domainSettingsService.blockedInteractionsUris$ = of(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getUrlEquivalentDomains", () => {
|
describe("getUrlEquivalentDomains", () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { map, Observable } from "rxjs";
|
import { map, Observable, switchMap, of } from "rxjs";
|
||||||
|
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NeverDomains,
|
NeverDomains,
|
||||||
@@ -8,6 +10,7 @@ import {
|
|||||||
UriMatchStrategySetting,
|
UriMatchStrategySetting,
|
||||||
UriMatchStrategy,
|
UriMatchStrategy,
|
||||||
} from "../../models/domain/domain-service";
|
} from "../../models/domain/domain-service";
|
||||||
|
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import {
|
import {
|
||||||
DOMAIN_SETTINGS_DISK,
|
DOMAIN_SETTINGS_DISK,
|
||||||
@@ -23,10 +26,20 @@ const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", {
|
|||||||
deserializer: (value: boolean) => value ?? true,
|
deserializer: (value: boolean) => value ?? true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Domain exclusion list for notifications
|
||||||
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
|
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
|
||||||
deserializer: (value: NeverDomains) => value ?? null,
|
deserializer: (value: NeverDomains) => value ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Domain exclusion list for content script injections
|
||||||
|
const BLOCKED_INTERACTIONS_URIS = new KeyDefinition(
|
||||||
|
DOMAIN_SETTINGS_DISK,
|
||||||
|
"blockedInteractionsUris",
|
||||||
|
{
|
||||||
|
deserializer: (value: NeverDomains) => value ?? null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
|
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
|
||||||
deserializer: (value: EquivalentDomains) => value ?? null,
|
deserializer: (value: EquivalentDomains) => value ?? null,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
@@ -41,15 +54,45 @@ const DEFAULT_URI_MATCH_STRATEGY = new UserKeyDefinition(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Domain Settings service; provides client settings state for "active client view" URI concerns
|
||||||
|
*/
|
||||||
export abstract class DomainSettingsService {
|
export abstract class DomainSettingsService {
|
||||||
|
/**
|
||||||
|
* Indicates if the favicons for ciphers' URIs should be shown instead of a placeholder
|
||||||
|
*/
|
||||||
showFavicons$: Observable<boolean>;
|
showFavicons$: Observable<boolean>;
|
||||||
setShowFavicons: (newValue: boolean) => Promise<void>;
|
setShowFavicons: (newValue: boolean) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-specified URIs for which the client notifications should not appear
|
||||||
|
*/
|
||||||
neverDomains$: Observable<NeverDomains>;
|
neverDomains$: Observable<NeverDomains>;
|
||||||
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
|
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-specified URIs for which client content script injections should not occur, and the state
|
||||||
|
* of banner/notice visibility for those domains within the client
|
||||||
|
*/
|
||||||
|
blockedInteractionsUris$: Observable<NeverDomains>;
|
||||||
|
setBlockedInteractionsUris: (newValue: NeverDomains) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URIs which should be treated as equivalent to each other for various concerns (autofill, etc)
|
||||||
|
*/
|
||||||
equivalentDomains$: Observable<EquivalentDomains>;
|
equivalentDomains$: Observable<EquivalentDomains>;
|
||||||
setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise<void>;
|
setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-specified default for URI-matching strategies (for example, when determining relevant
|
||||||
|
* ciphers for an active browser tab). Can be overridden by cipher-specific settings.
|
||||||
|
*/
|
||||||
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||||
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
|
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for the common resolution of a given URL against equivalent domains
|
||||||
|
*/
|
||||||
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
|
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,19 +103,37 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
|
|||||||
private neverDomainsState: GlobalState<NeverDomains>;
|
private neverDomainsState: GlobalState<NeverDomains>;
|
||||||
readonly neverDomains$: Observable<NeverDomains>;
|
readonly neverDomains$: Observable<NeverDomains>;
|
||||||
|
|
||||||
|
private blockedInteractionsUrisState: GlobalState<NeverDomains>;
|
||||||
|
readonly blockedInteractionsUris$: Observable<NeverDomains>;
|
||||||
|
|
||||||
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
|
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
|
||||||
readonly equivalentDomains$: Observable<EquivalentDomains>;
|
readonly equivalentDomains$: Observable<EquivalentDomains>;
|
||||||
|
|
||||||
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
|
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
|
||||||
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {
|
constructor(
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {
|
||||||
this.showFaviconsState = this.stateProvider.getGlobal(SHOW_FAVICONS);
|
this.showFaviconsState = this.stateProvider.getGlobal(SHOW_FAVICONS);
|
||||||
this.showFavicons$ = this.showFaviconsState.state$.pipe(map((x) => x ?? true));
|
this.showFavicons$ = this.showFaviconsState.state$.pipe(map((x) => x ?? true));
|
||||||
|
|
||||||
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
|
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
|
||||||
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
|
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
|
||||||
|
|
||||||
|
// Needs to be global to prevent pre-login injections
|
||||||
|
this.blockedInteractionsUrisState = this.stateProvider.getGlobal(BLOCKED_INTERACTIONS_URIS);
|
||||||
|
|
||||||
|
this.blockedInteractionsUris$ = this.configService
|
||||||
|
.getFeatureFlag$(FeatureFlag.BlockBrowserInjectionsByDomain)
|
||||||
|
.pipe(
|
||||||
|
switchMap((featureIsEnabled) =>
|
||||||
|
featureIsEnabled ? this.blockedInteractionsUrisState.state$ : of({} as NeverDomains),
|
||||||
|
),
|
||||||
|
map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : null)),
|
||||||
|
);
|
||||||
|
|
||||||
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
|
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
|
||||||
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
|
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
|
||||||
|
|
||||||
@@ -90,6 +151,10 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
|
|||||||
await this.neverDomainsState.update(() => newValue);
|
await this.neverDomainsState.update(() => newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setBlockedInteractionsUris(newValue: NeverDomains): Promise<void> {
|
||||||
|
await this.blockedInteractionsUrisState.update(() => newValue);
|
||||||
|
}
|
||||||
|
|
||||||
async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise<void> {
|
async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise<void> {
|
||||||
await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue);
|
await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export enum FeatureFlag {
|
|||||||
SSHKeyVaultItem = "ssh-key-vault-item",
|
SSHKeyVaultItem = "ssh-key-vault-item",
|
||||||
SSHAgent = "ssh-agent",
|
SSHAgent = "ssh-agent",
|
||||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||||
|
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
|
||||||
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
||||||
CipherKeyEncryption = "cipher-key-encryption",
|
CipherKeyEncryption = "cipher-key-encryption",
|
||||||
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
|
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
|
||||||
@@ -81,6 +82,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
||||||
[FeatureFlag.SSHAgent]: FALSE,
|
[FeatureFlag.SSHAgent]: FALSE,
|
||||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||||
|
[FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE,
|
||||||
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
||||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||||
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
|
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ export const UriMatchStrategy = {
|
|||||||
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
|
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
|
||||||
|
|
||||||
// using uniqueness properties of object shape over Set for ease of state storability
|
// using uniqueness properties of object shape over Set for ease of state storability
|
||||||
export type NeverDomains = { [id: string]: null };
|
export type NeverDomains = { [id: string]: null | { bannerIsDismissed?: boolean } };
|
||||||
export type EquivalentDomains = string[][];
|
export type EquivalentDomains = string[][];
|
||||||
|
|||||||
Reference in New Issue
Block a user