Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-13115] Allow users to disable extension content script injections by domain #11826

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

jprusik
Copy link
Contributor

@jprusik jprusik commented Nov 1, 2024

🎟️ Tracking

PM-13115

📔 Objective

Create a concept of disabled domains for the Bitwarden extension; unlike the excluded domains in the notification settings, a page belonging to the disabled domain list would prevent the Bitwarden extension from interacting with the page at all.

Notes

  • This feature is gated by the block-browser-injections-by-domain feature flag
  • While this work is functionally complete, there are follow up UX/design tasks (see tracked tasks in the ticket) that must be completed before this is released (via feature-flag)
  • This would not replace or otherwise impact the “excluded domains” concept for notifications (at least not within the scope of this PR; see comment)
  • Changes made to the blocked domains will not be reflected until the next content script injection to the page (typically a page navigation or refresh)

Screen capture

Kapture.2024-12-16.at.11.09.36.mp4
Kapture.2024-12-16.at.11.28.39.mp4

Screenshot 2024-12-16 at 11 02 26 AM
Screenshot 2024-12-16 at 11 32 47 AM

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@jprusik jprusik self-assigned this Nov 1, 2024
@jprusik jprusik force-pushed the pm-13115 branch 2 times, most recently from 77d3472 to e8ddd67 Compare November 1, 2024 19:53
Copy link
Contributor

github-actions bot commented Nov 11, 2024

Logo
Checkmarx One – Scan Summary & Detailsb588e267-fc91-496b-b64c-714c24601841

No New Or Fixed Issues Found

@jprusik jprusik force-pushed the pm-13115 branch 3 times, most recently from 308c302 to 2695a47 Compare December 16, 2024 14:51
@@ -67,7 +66,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
JslibModule,
LinkModule,
PopOutComponent,
PopupFooterComponent,
Copy link
Contributor Author

@jprusik jprusik Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PopupFooterComponent was not in use anywhere here or in the template

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is largely a copy of apps/browser/src/autofill/popup/settings/excluded-domains.component.ts

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have an open bug for the existing UI here PM-13808 with the input styling. Do you think we can get that fixed so we don't duplicate the UI into this new component?

@@ -3,6 +3,7 @@
[ciphers]="ciphers"
[title]="'autofillSuggestions' | i18n"
[showRefresh]="showRefresh"
[sectionIndicators]="sectionIndicators"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This concept allows for the conditional addition of icon indicators alongside the section title (in this case, an informational icon indicating the view contains ciphers for a domain that has been added to the user's block list).

Comment on lines +25 to +32
<bit-banner
*ngIf="vaultState !== VaultStateEnum.Empty && showScriptInjectionIsBlockedBanner"
id="domain-script-injection-blocked-banner"
bannerType="info"
(onClose)="handleScriptInjectionIsBlockedBannerDismiss()"
>
{{ "autofillBlockedNotice" | i18n }}
</bit-banner>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up work will ultimately leverage the pop up page component slot added in #12279

this.domainSettingsService
.setBlockedInteractionsUris({
...blockedURIs,
[this.autofillTabHostname]: { bannerIsDismissed: true },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the existing NeverDomains data shape give us tons of flexibility in the future if we implement more granular injection-blocking options.

As it is, we could merge the neverDomains state concept (expressed in the interface as "Excluded Domains" and currently used to allow users to block the notification bar on specified domains) into blockedInteractionsUris concept with a state migration.

For example (not a well-considered data shape, just for illustrative purposes):

{
  "banking-portal.real-tld": {
    "blockedInjections": null, // all injections blocked
    "dismissedMessages": ['autofillBlockedBanner']
  }, 
  "duckduckgo.com": {
    "blockedInjections": ["notifications", "inline-menu"], 
    "dismissedMessages": []
  }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example (not a well-considered data shape, just for illustrative purposes):

💭 You might want to update this example to account for our "no-null" guidance. If you want to keep the example short, I'd just swap out the null for true or "all" (no list).

A more comprehensive example that would be easier to port to Rust (as an enum) uses a discriminated union.

type ScriptType = "notifications" | "inline-menu";
type Injectable = { type: "allow-all" } | { type: "deny-list", injections: ScriptType[] } | { type: "deny-all" };

Comment on lines +57 to +58
if (!injectionAllowedInTab) {
throw new Error("This URI of this tab is on the blocked domains list.");
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like
Screenshot 2024-12-16 at 9 51 47 AM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is purposeful blocking, no? Do we want a visible error like this?

@jprusik jprusik force-pushed the pm-13115 branch 6 times, most recently from fe32841 to 1dd180e Compare December 17, 2024 16:25
@jprusik jprusik changed the title [PM-13115] Allow users to disable the Bitwarden extension by domain [PM-13115] Allow users to disable extension content script injections by domain Dec 19, 2024
@jprusik jprusik marked this pull request as ready for review December 19, 2024 21:05
@jprusik jprusik requested review from a team as code owners December 19, 2024 21:05
Copy link

codecov bot commented Dec 19, 2024

Codecov Report

Attention: Patch coverage is 14.37908% with 131 lines in your changes missing coverage. Please review.

Project coverage is 33.54%. Comparing base (997d40f) to head (ab2f085).
Report is 2 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...tofill/popup/settings/blocked-domains.component.ts 0.00% 73 Missing ⚠️
...vault/popup/components/vault/vault-v2.component.ts 0.00% 28 Missing ⚠️
...-container/vault-list-items-container.component.ts 0.00% 7 Missing ⚠️
...ofill/popup/settings/excluded-domains.component.ts 0.00% 6 Missing ⚠️
...n/src/autofill/services/domain-settings.service.ts 53.84% 5 Missing and 1 partial ⚠️
...-list-items/autofill-vault-list-items.component.ts 0.00% 3 Missing ⚠️
...c/autofill/popup/settings/autofill-v1.component.ts 0.00% 2 Missing ⚠️
.../src/autofill/popup/settings/autofill.component.ts 0.00% 2 Missing ⚠️
...atform/services/browser-script-injector.service.ts 83.33% 0 Missing and 2 partials ⚠️
apps/browser/src/background/main.background.ts 0.00% 1 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11826      +/-   ##
==========================================
- Coverage   33.56%   33.54%   -0.03%     
==========================================
  Files        2926     2927       +1     
  Lines       91527    91658     +131     
  Branches    17395    17422      +27     
==========================================
+ Hits        30721    30744      +23     
- Misses      58392    58497     +105     
- Partials     2414     2417       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@audreyality audreyality self-requested a review December 20, 2024 15:46
@@ -93,6 +102,7 @@ describe("FilelessImporterBackground ", () => {
});

it("posts a message to the port indicating that the fileless import feature is disabled if the user's auth status is not unlocked", async () => {
(firstValueFrom as jest.Mock).mockResolvedValue(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ firstValueFrom may be used in many places in the code. This line makes every call return false, which is almost certainly incorrect behavior. The observable passed to firstValueFrom should be mocked instead.

🤔 If firstValueFrom is being called from within an autofill API, consider implementing a fake for the API or some other means to simulate the awaited observable. If it's being called from tools code, then I'll follow up with the tools team to fix the test.

Comment on lines +62 to +63
blockedInteractionsUris$: Observable<NeverDomains>;
setBlockedInteractionsUris: (newValue: NeverDomains) => Promise<void>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Since these members are exported from this module and overridden in numerous tests, their behavior should be documented.

Comment on lines +100 to +111
this.blockedInteractionsUris$ = combineLatest([
this.blockedInteractionsUrisState.state$,
this.configService?.getFeatureFlag$(FeatureFlag.BlockBrowserInjectionsByDomain),
]).pipe(
map(([blockedUris, blockBrowserInjectionsByDomainEnabled]) => {
if (!blockBrowserInjectionsByDomainEnabled) {
return null;
}

return blockedUris ?? null;
}),
);
Copy link
Member

@audreyality audreyality Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 This can be simplified if configService becomes required.

this.configService.getFeatureFlag$(FeatureFlag.BlockBrowserInjectionsByDomain).pipe(
  switchMap((enabled) => enabled ? this.blockedInteractionsUrisState.state$ : of([])),
  map(blocked => blocked ?? [])
);

The main benefit of this is that it won't emit false updates from the state service when the feature is disabled.

One thing to note, however, is that combineLatest only completes when both of its input streams complete. Since blockedInteractionsUrisState.state$ never completes, neither will blockedInteractionsUris$.

switchMap, on the other hand, completes once its source and argument streams complete. There's a subtle difference between the constructions because of([]) completes after emitting its last argument. If getFeatureFlag$ completes after emitting false then the whole observable will complete.

Ultimately, which construction works best for you depends on how you consume this.blockedInteractionsUris$. If you want it to update as soon as the flag becomes available, then combineLatest (with logic to filter false updates) is preferable. Your notes indicate that you need to navigate in order for it to pick up the new settings, however, which leads me to believe the switchMap approach more closely models your expectations.

📝 I also eliminated null in my example code per ADR-14.

Copy link
Member

@coltonhurst coltonhurst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two questions just to start!

"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."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any way to deep link to the relevant settings page in the notice?

Comment on lines +57 to +58
if (!injectionAllowedInTab) {
throw new Error("This URI of this tab is on the blocked domains list.");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is purposeful blocking, no? Do we want a visible error like this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants