1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-30 16:23:53 +00:00

Merge branch 'main' into beeep/dev-container

This commit is contained in:
Conner Turnbull
2026-01-29 08:53:01 -05:00
committed by GitHub
7 changed files with 48 additions and 38 deletions

View File

@@ -2,11 +2,14 @@
A library for native autofill providers to interact with a host Bitwarden desktop app.
In this desktop context, "native autofill providers" are operating system frameworks or APIs (like the macOS [autofill framework](https://developer.apple.com/documentation/Security/password-autofill)) that allow Bitwarden to provide provide user credentials for things like autofill, passkey operations, etc.
# Explainer: Mac OS Native Passkey Provider
This document describes the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context.
## The high level
MacOS has native APIs (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in the PR referenced above, we only provide passkeys).
Weve written a Swift-based native autofill-extension. Its bundled in the app-bundle in PlugIns, similar to the safari-extension.
@@ -16,7 +19,7 @@ This swift extension currently communicates with our Electron app through IPC ba
Footnotes:
* We're not using the IPC framework as the implementation pre-dates the IPC framework.
* Alternatives like XPC or CFMessagePort may have better support for when the app is sandboxed.
* Alternatives like XPC or CFMessagePort may have better support for when the app is sandboxed.
Electron receives the messages and passes it to Angular (through the electron-renderer event system).
@@ -26,7 +29,7 @@ Our existing fido2 services in the renderer respond to events, displaying UI as
We utilize the same FIDO2 implementation and interface that is already present for our browser authentication. It was designed by @coroiu with multiple ui environments' in mind.
Therefore, a lot of the plumbing is implemented in /autofill/services/desktop-fido2-user-interface.service.ts, which implements the interface that our fido2 authenticator/client expects to drive UI related behaviors.
Therefore, a lot of the plumbing is implemented in /autofill/services/desktop-fido2-user-interface.service.ts, which implements the interface that our fido2 authenticator/client expects to drive UI related behaviors.
Weve also implemented a couple FIDO2 UI components to handle registration/sign in flows, but also improved the “modal mode” of the desktop app.

View File

@@ -22,7 +22,11 @@
></button>
}
@for (childFolder of folder().children; track childFolder.node.id) {
<app-folder-filter [folder]="childFolder" [activeFilter]="activeFilter()" />
<app-folder-filter
[folder]="childFolder"
[activeFilter]="activeFilter()"
(onEditFolder)="editFolder($event)"
/>
}
</bit-nav-group>
} @else {

View File

@@ -3,5 +3,5 @@
<bit-container>
<p>{{ "reportsDesc" | i18n }}</p>
<app-report-list [reports]="reports"></app-report-list>
<app-report-list [reports]="reports()"></app-report-list>
</bit-container>

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { ChangeDetectionStrategy, Component, OnInit, signal } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -16,7 +16,7 @@ import { ReportEntry, ReportVariant } from "../shared";
standalone: false,
})
export class ReportsHomeComponent implements OnInit {
reports: ReportEntry[];
readonly reports = signal<ReportEntry[]>([]);
constructor(
private billingAccountProfileStateService: BillingAccountProfileStateService,
@@ -32,7 +32,7 @@ export class ReportsHomeComponent implements OnInit {
? ReportVariant.Enabled
: ReportVariant.RequiresPremium;
this.reports = [
this.reports.set([
{
...reports[ReportType.ExposedPasswords],
variant: reportRequiresPremium,
@@ -57,6 +57,6 @@ export class ReportsHomeComponent implements OnInit {
...reports[ReportType.DataBreach],
variant: ReportVariant.Enabled,
},
];
]);
}
}

View File

@@ -1,7 +1,7 @@
<div
class="tw-inline-grid tw-place-items-stretch tw-place-content-center tw-grid-cols-1 @xl:tw-grid-cols-2 @4xl:tw-grid-cols-3 tw-gap-4 [&_a]:tw-max-w-none @5xl:[&_a]:tw-max-w-72"
>
@for (report of reports; track report) {
@for (report of reports(); track report) {
<div>
<app-report-card
[title]="report.title | i18n"

View File

@@ -1,18 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { ReportEntry } from "../models/report-entry";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-report-list",
templateUrl: "report-list.component.html",
standalone: false,
})
export class ReportListComponent {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() reports: ReportEntry[];
readonly reports = input<ReportEntry[]>([]);
}

View File

@@ -2,7 +2,6 @@ import {
ChangeDetectionStrategy,
Component,
DestroyRef,
Injector,
OnInit,
Signal,
computed,
@@ -11,7 +10,7 @@ import {
input,
signal,
} from "@angular/core";
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
import { toSignal } from "@angular/core/rxjs-interop";
import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -59,6 +58,9 @@ export class PasswordChangeMetricComponent implements OnInit {
private readonly _tasks: Signal<SecurityTask[]> = signal<SecurityTask[]>([]);
private readonly _atRiskCipherIds: Signal<CipherId[]> = signal<CipherId[]>([]);
private readonly _hasCriticalApplications: Signal<boolean> = signal<boolean>(false);
private readonly _reportGeneratedAt: Signal<Date | undefined> = signal<Date | undefined>(
undefined,
);
// Computed properties
readonly tasksCount = computed(() => this._tasks().length);
@@ -80,8 +82,24 @@ export class PasswordChangeMetricComponent implements OnInit {
}
const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending);
const assignedIdSet = new Set(inProgressTasks.map((task) => task.cipherId));
const unassignedIds = atRiskIds.filter((id) => !assignedIdSet.has(id));
const inProgressTaskIds = new Set(inProgressTasks.map((task) => task.cipherId));
const reportGeneratedAt = this._reportGeneratedAt();
const completedTasksAfterReportGeneration = reportGeneratedAt
? tasks.filter(
(task) =>
task.status === SecurityTaskStatus.Completed &&
new Date(task.revisionDate) >= reportGeneratedAt,
)
: [];
const completedTaskIds = new Set(
completedTasksAfterReportGeneration.map((task) => task.cipherId),
);
// find cipher ids from last report that do not have a corresponding in progress task (awaiting password reset) OR completed task
const unassignedIds = atRiskIds.filter(
(id) => !inProgressTaskIds.has(id) && !completedTaskIds.has(id),
);
return unassignedIds.length;
});
@@ -109,36 +127,26 @@ export class PasswordChangeMetricComponent implements OnInit {
constructor(
private allActivitiesService: AllActivitiesService,
private i18nService: I18nService,
private injector: Injector,
private riskInsightsDataService: RiskInsightsDataService,
protected securityTasksService: AccessIntelligenceSecurityTasksService,
private toastService: ToastService,
) {
// Setup the _tasks signal by manually passing in the injector
this._tasks = toSignal(this.securityTasksService.tasks$, {
initialValue: [],
injector: this.injector,
});
// Setup the _atRiskCipherIds signal by manually passing in the injector
this._tasks = toSignal(this.securityTasksService.tasks$, { initialValue: [] });
this._atRiskCipherIds = toSignal(
this.riskInsightsDataService.criticalApplicationAtRiskCipherIds$,
{
initialValue: [],
injector: this.injector,
},
{ initialValue: [] },
);
this._hasCriticalApplications = toSignal(
this.riskInsightsDataService.criticalReportResults$.pipe(
takeUntilDestroyed(this.destroyRef),
map((report) => {
return report != null && (report.reportData?.length ?? 0) > 0;
}),
),
{
initialValue: false,
injector: this.injector,
},
{ initialValue: false },
);
this._reportGeneratedAt = toSignal(
this.riskInsightsDataService.enrichedReportData$.pipe(map((report) => report?.creationDate)),
{ initialValue: undefined },
);
effect(() => {