=>
+ ipcRenderer.invoke("biometric", {
+ action: BiometricAction.IsLinuxV2Enabled,
+ } satisfies BiometricMessage),
};
export default {
diff --git a/apps/desktop/src/services/biometric-message-handler.service.ts b/apps/desktop/src/services/biometric-message-handler.service.ts
index 6d07c4a2aa0..022ccffcc91 100644
--- a/apps/desktop/src/services/biometric-message-handler.service.ts
+++ b/apps/desktop/src/services/biometric-message-handler.service.ts
@@ -125,6 +125,11 @@ export class BiometricMessageHandlerService {
if (windowsV2Enabled) {
await this.biometricsService.enableWindowsV2Biometrics();
}
+
+ const linuxV2Enabled = await this.configService.getFeatureFlag(FeatureFlag.LinuxBiometricsV2);
+ if (linuxV2Enabled) {
+ await this.biometricsService.enableLinuxV2Biometrics();
+ }
}
async handleMessage(msg: LegacyMessageWrapper) {
diff --git a/apps/desktop/src/types/biometric-message.ts b/apps/desktop/src/types/biometric-message.ts
index a0a3967f468..62aa9fb1ce2 100644
--- a/apps/desktop/src/types/biometric-message.ts
+++ b/apps/desktop/src/types/biometric-message.ts
@@ -19,6 +19,9 @@ export enum BiometricAction {
EnableWindowsV2 = "enableWindowsV2",
IsWindowsV2Enabled = "isWindowsV2Enabled",
+
+ EnableLinuxV2 = "enableLinuxV2",
+ IsLinuxV2Enabled = "isLinuxV2Enabled",
}
export type BiometricMessage =
diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts
index 78a6d6c0dac..62f6539cc16 100644
--- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts
+++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts
@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
-import { concatMap, filter, firstValueFrom, lastValueFrom, map, switchMap, takeUntil } from "rxjs";
+import { concatMap, firstValueFrom, lastValueFrom, map, of, switchMap, takeUntil, tap } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
@@ -143,17 +143,23 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
getUserId,
switchMap((userId) => this.providerService.get$(this.organization.providerId, userId)),
map((provider) => provider != null && provider.canManageUsers),
- filter((result) => result),
- switchMap(() => this.apiService.getProviderUsers(this.organization.id)),
- map((providerUsersResponse) =>
- providerUsersResponse.data.forEach((u) => {
- const name = this.userNamePipe.transform(u);
- this.orgUsersUserIdMap.set(u.userId, {
- name: `${name} (${this.organization.providerName})`,
- email: u.email,
+ switchMap((canManage) => {
+ if (canManage) {
+ return this.apiService.getProviderUsers(this.organization.providerId);
+ }
+ return of(null);
+ }),
+ tap((providerUsersResponse) => {
+ if (providerUsersResponse) {
+ providerUsersResponse.data.forEach((u) => {
+ const name = this.userNamePipe.transform(u);
+ this.orgUsersUserIdMap.set(u.userId, {
+ name: `${name} (${this.organization.providerName})`,
+ email: u.email,
+ });
});
- }),
- ),
+ }
+ }),
),
);
} catch (e) {
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts
index 7373e1ff888..9b46e228af9 100644
--- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts
@@ -10,6 +10,7 @@ export { RestrictedItemTypesPolicy } from "./restricted-item-types.component";
export { SendOptionsPolicy } from "./send-options.component";
export { SingleOrgPolicy } from "./single-org.component";
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
+export { UriMatchDefaultPolicy } from "./uri-match-default.component";
export {
vNextOrganizationDataOwnershipPolicy,
vNextOrganizationDataOwnershipPolicyComponent,
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/uri-match-default.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/uri-match-default.component.html
new file mode 100644
index 00000000000..399a4ad2dcd
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/uri-match-default.component.html
@@ -0,0 +1,22 @@
+
+ {{ "requireSsoPolicyReq" | i18n }}
+
+
+
+
+ {{ "turnOn" | i18n }}
+
+
+
+
+ {{ "uriMatchDetectionOptionsLabel" | i18n }}
+
+
+
+
+
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/uri-match-default.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/uri-match-default.component.ts
new file mode 100644
index 00000000000..5c0b667bea2
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/uri-match-default.component.ts
@@ -0,0 +1,72 @@
+import { Component, ChangeDetectionStrategy } from "@angular/core";
+import { FormBuilder, FormControl, Validators } from "@angular/forms";
+
+import { PolicyType } from "@bitwarden/common/admin-console/enums";
+import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
+import {
+ UriMatchStrategy,
+ UriMatchStrategySetting,
+} from "@bitwarden/common/models/domain/domain-service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+
+import { SharedModule } from "../../../../shared";
+import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
+
+export class UriMatchDefaultPolicy extends BasePolicyEditDefinition {
+ name = "uriMatchDetectionPolicy";
+ description = "uriMatchDetectionPolicyDesc";
+ type = PolicyType.UriMatchDefaults;
+ component = UriMatchDefaultPolicyComponent;
+}
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: "uri-match-default.component.html",
+ imports: [SharedModule],
+})
+export class UriMatchDefaultPolicyComponent extends BasePolicyEditComponent {
+ uriMatchOptions: { label: string; value: UriMatchStrategySetting | null; disabled?: boolean }[];
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private i18nService: I18nService,
+ ) {
+ super();
+
+ this.data = this.formBuilder.group({
+ uriMatchDetection: new FormControl(UriMatchStrategy.Domain, {
+ validators: [Validators.required],
+ nonNullable: true,
+ }),
+ });
+
+ this.uriMatchOptions = [
+ { label: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
+ { label: i18nService.t("host"), value: UriMatchStrategy.Host },
+ { label: i18nService.t("exact"), value: UriMatchStrategy.Exact },
+ { label: i18nService.t("never"), value: UriMatchStrategy.Never },
+ ];
+ }
+
+ protected loadData() {
+ const uriMatchDetection = this.policyResponse?.data?.uriMatchDetection;
+
+ this.data?.patchValue({
+ uriMatchDetection: uriMatchDetection,
+ });
+ }
+
+ protected buildRequestData() {
+ return {
+ uriMatchDetection: this.data?.value?.uriMatchDetection,
+ };
+ }
+
+ async buildRequest(): Promise {
+ const request = await super.buildRequest();
+ if (request.data?.uriMatchDetection == null) {
+ throw new Error(this.i18nService.t("invalidUriMatchDefaultPolicySetting"));
+ }
+
+ return request;
+ }
+}
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts
index ca44818764c..a4bdece0a7b 100644
--- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts
@@ -13,6 +13,7 @@ import {
SendOptionsPolicy,
SingleOrgPolicy,
TwoFactorAuthenticationPolicy,
+ UriMatchDefaultPolicy,
vNextOrganizationDataOwnershipPolicy,
} from "./policy-edit-definitions";
@@ -34,5 +35,6 @@ export const ossPolicyEditRegister: BasePolicyEditDefinition[] = [
new SendOptionsPolicy(),
new RestrictedItemTypesPolicy(),
new DesktopAutotypeDefaultSettingPolicy(),
+ new UriMatchDefaultPolicy(),
new AutoConfirmPolicy(),
];
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 2e0fd99bbcc..634e60db0c5 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -343,6 +343,12 @@
"reviewNow": {
"message": "Review now"
},
+ "allCaughtUp": {
+ "message": "All caught up!"
+ },
+ "noNewApplicationsToReviewAtThisTime": {
+ "message": "No new applications to review at this time"
+ },
"prioritizeCriticalApplications": {
"message": "Prioritize critical applications"
},
@@ -5875,6 +5881,19 @@
"message": "Always show member’s email address with recipients when creating or editing a Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
+ "uriMatchDetectionPolicy": {
+ "message": "Default URI match detection"
+ },
+ "uriMatchDetectionPolicyDesc": {
+ "message": "Determine when logins are suggested for autofill. Admins and owners are exempt from this policy."
+ },
+ "uriMatchDetectionOptionsLabel": {
+ "message": "Default URI match detection"
+ },
+ "invalidUriMatchDefaultPolicySetting": {
+ "message": "Please select a valid URI match detection option.",
+ "description": "Error message displayed when a user attempts to save URI match detection policy settings with an invalid selection."
+ },
"modifiedPolicyId": {
"message": "Modified policy $ID$.",
"placeholders": {
@@ -9653,7 +9672,7 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
- "uriMatchDefaultStrategyHint": {
+ "uriMatchDefaultStrategyHint": {
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
},
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html
index 756907d24e6..73f98034f0a 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html
@@ -2,7 +2,7 @@
{{ title }}
@if (iconClass) {
-
+
}
{{ cardMetrics }}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts
index 427e7262f50..24d931165a7 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts
@@ -58,6 +58,14 @@ export class ActivityCardComponent {
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() iconClass: string | null = null;
+ /**
+ * CSS class for icon color (e.g., "tw-text-success", "tw-text-muted").
+ * Defaults to "tw-text-muted" if not provided.
+ */
+ // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
+ // eslint-disable-next-line @angular-eslint/prefer-signals
+ @Input() iconColorClass: string = "tw-text-muted";
+
/**
* Button text. If provided, a button will be displayed instead of a navigation link.
*/
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html
index 82137c9acca..8cdb927ab65 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html
@@ -45,10 +45,19 @@
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts
index b12a8fffedb..150c66ad2d4 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts
@@ -42,6 +42,9 @@ export class AllActivityComponent implements OnInit {
newApplicationsCount = 0;
newApplications: string[] = [];
passwordChangeMetricHasProgressBar = false;
+ allAppsHaveReviewDate = false;
+ isAllCaughtUp = false;
+ hasLoadedApplicationData = false;
destroyRef = inject(DestroyRef);
@@ -79,6 +82,7 @@ export class AllActivityComponent implements OnInit {
.subscribe((newApps) => {
this.newApplications = newApps;
this.newApplicationsCount = newApps.length;
+ this.updateIsAllCaughtUp();
});
this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$
@@ -86,9 +90,39 @@ export class AllActivityComponent implements OnInit {
.subscribe((hasProgressBar) => {
this.passwordChangeMetricHasProgressBar = hasProgressBar;
});
+
+ this.dataService.enrichedReportData$
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((enrichedData) => {
+ if (enrichedData?.applicationData && enrichedData.applicationData.length > 0) {
+ this.hasLoadedApplicationData = true;
+ // Check if all apps have a review date (not null and not undefined)
+ this.allAppsHaveReviewDate = enrichedData.applicationData.every(
+ (app) => app.reviewedDate !== null && app.reviewedDate !== undefined,
+ );
+ } else {
+ this.hasLoadedApplicationData = enrichedData !== null;
+ this.allAppsHaveReviewDate = false;
+ }
+ this.updateIsAllCaughtUp();
+ });
}
}
+ /**
+ * Updates the isAllCaughtUp flag based on current state.
+ * Only shows "All caught up!" when:
+ * - Data has been loaded (hasLoadedApplicationData is true)
+ * - No new applications need review
+ * - All apps have a review date
+ */
+ private updateIsAllCaughtUp(): void {
+ this.isAllCaughtUp =
+ this.hasLoadedApplicationData &&
+ this.newApplicationsCount === 0 &&
+ this.allAppsHaveReviewDate;
+ }
+
/**
* Handles the review new applications button click.
* Opens a dialog showing the list of new applications that can be marked as critical.
diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts
index d6ab8851ad7..fa7a37ba732 100644
--- a/libs/common/src/autofill/services/domain-settings.service.ts
+++ b/libs/common/src/autofill/services/domain-settings.service.ts
@@ -166,7 +166,7 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
if (!policy?.enabled || policy?.data == null) {
return null;
}
- const data = policy.data?.defaultUriMatchStrategy;
+ const data = policy.data?.uriMatchDetection;
// Validate that data is a valid UriMatchStrategy value
return Object.values(UriMatchStrategy).includes(data) ? data : null;
}),
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index bfb40aff106..e4667c73603 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -37,6 +37,7 @@ export enum FeatureFlag {
ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings",
PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption",
WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2",
+ LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2",
UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data",
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
@@ -124,6 +125,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.ForceUpdateKDFSettings]: FALSE,
[FeatureFlag.PM25174_DisableType0Decryption]: FALSE,
[FeatureFlag.WindowsBiometricsV2]: FALSE,
+ [FeatureFlag.LinuxBiometricsV2]: FALSE,
[FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE,
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,