diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index add53a0cd33..1153ad58719 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -232,7 +232,7 @@ {{ "enableAutoTotpCopy" | i18n }} - + {{ "clearClipboard" | i18n }} - + {{ "defaultUriMatchDetection" | i18n }} - - {{ "settingDisabledByPolicy" | i18n }} - {{ hints[0] | i18n }} diff --git a/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts b/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts index 0fe3d7e95e7..04a2f389781 100644 --- a/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts @@ -18,4 +18,7 @@ export abstract class DesktopBiometricsService extends BiometricsService { /* Enables the v2 biometrics re-write. This will stay enabled until the application is restarted. */ abstract enableWindowsV2Biometrics(): Promise; abstract isWindowsV2BiometricsEnabled(): Promise; + /* Enables the v2 biometrics re-write. This will stay enabled until the application is restarted. */ + abstract enableLinuxV2Biometrics(): Promise; + abstract isLinuxV2BiometricsEnabled(): Promise; } diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts index 24bb5495da0..db7c7c8f7fa 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts @@ -62,6 +62,10 @@ export class MainBiometricsIPCListener { return await this.biometricService.enableWindowsV2Biometrics(); case BiometricAction.IsWindowsV2Enabled: return await this.biometricService.isWindowsV2BiometricsEnabled(); + case BiometricAction.EnableLinuxV2: + return await this.biometricService.enableLinuxV2Biometrics(); + case BiometricAction.IsLinuxV2Enabled: + return await this.biometricService.isLinuxV2BiometricsEnabled(); default: return; } diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts index d1aff17646a..da532828314 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts @@ -10,13 +10,14 @@ import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-manageme import { WindowMain } from "../../main/window.main"; import { DesktopBiometricsService } from "./desktop.biometrics.service"; -import { WindowsBiometricsSystem } from "./native-v2"; +import { LinuxBiometricsSystem, WindowsBiometricsSystem } from "./native-v2"; import { OsBiometricService } from "./os-biometrics.service"; export class MainBiometricsService extends DesktopBiometricsService { private osBiometricsService: OsBiometricService; private shouldAutoPrompt = true; private windowsV2BiometricsEnabled = false; + private linuxV2BiometricsEnabled = false; constructor( private i18nService: I18nService, @@ -170,4 +171,16 @@ export class MainBiometricsService extends DesktopBiometricsService { async isWindowsV2BiometricsEnabled(): Promise { return this.windowsV2BiometricsEnabled; } + + async enableLinuxV2Biometrics(): Promise { + if (this.platform === "linux" && !this.linuxV2BiometricsEnabled) { + this.logService.info("[BiometricsMain] Loading native biometrics module v2 for linux"); + this.osBiometricsService = new LinuxBiometricsSystem(); + this.linuxV2BiometricsEnabled = true; + } + } + + async isLinuxV2BiometricsEnabled(): Promise { + return this.linuxV2BiometricsEnabled; + } } diff --git a/apps/desktop/src/key-management/biometrics/native-v2/index.ts b/apps/desktop/src/key-management/biometrics/native-v2/index.ts index 030224bbd74..94de850b759 100644 --- a/apps/desktop/src/key-management/biometrics/native-v2/index.ts +++ b/apps/desktop/src/key-management/biometrics/native-v2/index.ts @@ -1 +1,2 @@ export { default as WindowsBiometricsSystem } from "./os-biometrics-windows.service"; +export { default as LinuxBiometricsSystem } from "./os-biometrics-linux.service"; diff --git a/apps/desktop/src/key-management/biometrics/native-v2/os-biometrics-linux.service.spec.ts b/apps/desktop/src/key-management/biometrics/native-v2/os-biometrics-linux.service.spec.ts new file mode 100644 index 00000000000..91e2caba0cb --- /dev/null +++ b/apps/desktop/src/key-management/biometrics/native-v2/os-biometrics-linux.service.spec.ts @@ -0,0 +1,96 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { biometrics_v2, passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus } from "@bitwarden/key-management"; + +import OsBiometricsServiceLinux from "./os-biometrics-linux.service"; + +jest.mock("@bitwarden/desktop-napi", () => ({ + biometrics_v2: { + initBiometricSystem: jest.fn(() => "mockSystem"), + provideKey: jest.fn(), + unenroll: jest.fn(), + unlock: jest.fn(), + authenticate: jest.fn(), + authenticateAvailable: jest.fn(), + unlockAvailable: jest.fn(), + }, + passwords: { + isAvailable: jest.fn(), + }, +})); + +const mockKey = new Uint8Array(64); + +jest.mock("../../../utils", () => ({ + isFlatpak: jest.fn(() => false), + isLinux: jest.fn(() => true), + isSnapStore: jest.fn(() => false), +})); + +describe("OsBiometricsServiceLinux", () => { + const userId = "user-id" as UserId; + const key = { toEncoded: () => ({ buffer: Buffer.from(mockKey) }) } as SymmetricCryptoKey; + let service: OsBiometricsServiceLinux; + + beforeEach(() => { + service = new OsBiometricsServiceLinux(); + jest.clearAllMocks(); + }); + + it("should set biometric key", async () => { + await service.setBiometricKey(userId, key); + expect(biometrics_v2.provideKey).toHaveBeenCalled(); + }); + + it("should delete biometric key", async () => { + await service.deleteBiometricKey(userId); + expect(biometrics_v2.unenroll).toHaveBeenCalled(); + }); + + it("should get biometric key", async () => { + (biometrics_v2.unlock as jest.Mock).mockResolvedValue(mockKey); + const result = await service.getBiometricKey(userId); + expect(result).toBeInstanceOf(SymmetricCryptoKey); + }); + + it("should return null if no biometric key", async () => { + (biometrics_v2.unlock as jest.Mock).mockResolvedValue(null); + const result = await service.getBiometricKey(userId); + expect(result).toBeNull(); + }); + + it("should authenticate biometric", async () => { + (biometrics_v2.authenticate as jest.Mock).mockResolvedValue(true); + const result = await service.authenticateBiometric(); + expect(result).toBe(true); + }); + + it("should check if biometrics is supported", async () => { + (passwords.isAvailable as jest.Mock).mockResolvedValue(true); + const result = await service.supportsBiometrics(); + expect(result).toBe(true); + }); + + it("should check if setup is needed", async () => { + (biometrics_v2.authenticateAvailable as jest.Mock).mockResolvedValue(false); + const result = await service.needsSetup(); + expect(result).toBe(true); + }); + + it("should check if can auto setup", async () => { + const result = await service.canAutoSetup(); + expect(result).toBe(true); + }); + + it("should get biometrics first unlock status for user", async () => { + (biometrics_v2.unlockAvailable as jest.Mock).mockResolvedValue(true); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.Available); + }); + + it("should return false for hasPersistentKey", async () => { + const result = await service.hasPersistentKey(userId); + expect(result).toBe(false); + }); +}); diff --git a/apps/desktop/src/key-management/biometrics/native-v2/os-biometrics-linux.service.ts b/apps/desktop/src/key-management/biometrics/native-v2/os-biometrics-linux.service.ts new file mode 100644 index 00000000000..110db23ec79 --- /dev/null +++ b/apps/desktop/src/key-management/biometrics/native-v2/os-biometrics-linux.service.ts @@ -0,0 +1,118 @@ +import { spawn } from "child_process"; + +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { biometrics_v2, passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus } from "@bitwarden/key-management"; + +import { isSnapStore, isFlatpak, isLinux } from "../../../utils"; +import { OsBiometricService } from "../os-biometrics.service"; + +const polkitPolicy = ` + + + + + Unlock Bitwarden + Authenticate to unlock Bitwarden + + no + no + auth_self + + +`; +const policyFileName = "com.bitwarden.Bitwarden.policy"; +const policyPath = "/usr/share/polkit-1/actions/"; + +export default class OsBiometricsServiceLinux implements OsBiometricService { + private biometricsSystem: biometrics_v2.BiometricLockSystem; + + constructor() { + this.biometricsSystem = biometrics_v2.initBiometricSystem(); + } + + async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { + await biometrics_v2.provideKey( + this.biometricsSystem, + userId, + Buffer.from(key.toEncoded().buffer), + ); + } + + async deleteBiometricKey(userId: UserId): Promise { + await biometrics_v2.unenroll(this.biometricsSystem, userId); + } + + async getBiometricKey(userId: UserId): Promise { + const result = await biometrics_v2.unlock(this.biometricsSystem, userId, Buffer.from("")); + return result ? new SymmetricCryptoKey(Uint8Array.from(result)) : null; + } + + async authenticateBiometric(): Promise { + return await biometrics_v2.authenticate( + this.biometricsSystem, + Buffer.from(""), + "Authenticate to unlock", + ); + } + + async supportsBiometrics(): Promise { + // We assume all linux distros have some polkit implementation + // that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup. + // Snap does not have access at the moment to polkit + // This could be dynamically detected on dbus in the future. + // We should check if a libsecret implementation is available on the system + // because otherwise we cannot offlod the protected userkey to secure storage. + return await passwords.isAvailable(); + } + + async needsSetup(): Promise { + if (isSnapStore()) { + return false; + } + + // check whether the polkit policy is loaded via dbus call to polkit + return !(await biometrics_v2.authenticateAvailable(this.biometricsSystem)); + } + + async canAutoSetup(): Promise { + // We cannot auto setup on snap or flatpak since the filesystem is sandboxed. + // The user needs to manually set up the polkit policy outside of the sandbox + // since we allow access to polkit via dbus for the sandboxed clients, the authentication works from + // the sandbox, once the policy is set up outside of the sandbox. + return isLinux() && !isSnapStore() && !isFlatpak(); + } + + async runSetup(): Promise { + const process = spawn("pkexec", [ + "bash", + "-c", + `echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`, + ]); + + await new Promise((resolve, reject) => { + process.on("close", (code) => { + if (code !== 0) { + reject("Failed to set up polkit policy"); + } else { + resolve(null); + } + }); + }); + } + + async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { + return (await biometrics_v2.unlockAvailable(this.biometricsSystem, userId)) + ? BiometricsStatus.Available + : BiometricsStatus.UnlockNeeded; + } + + async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise {} + + async hasPersistentKey(userId: UserId): Promise { + return false; + } +} diff --git a/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts index bc3631ad1b8..63d2225b7c6 100644 --- a/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts @@ -84,4 +84,12 @@ export class RendererBiometricsService extends DesktopBiometricsService { async isWindowsV2BiometricsEnabled(): Promise { return await ipc.keyManagement.biometric.isWindowsV2BiometricsEnabled(); } + + async enableLinuxV2Biometrics(): Promise { + return await ipc.keyManagement.biometric.enableLinuxV2Biometrics(); + } + + async isLinuxV2BiometricsEnabled(): Promise { + return await ipc.keyManagement.biometric.isLinuxV2BiometricsEnabled(); + } } diff --git a/apps/desktop/src/key-management/preload.ts b/apps/desktop/src/key-management/preload.ts index a9565790b86..d317b1f6ce0 100644 --- a/apps/desktop/src/key-management/preload.ts +++ b/apps/desktop/src/key-management/preload.ts @@ -69,6 +69,14 @@ const biometric = { ipcRenderer.invoke("biometric", { action: BiometricAction.IsWindowsV2Enabled, } satisfies BiometricMessage), + enableLinuxV2Biometrics: (): Promise => + ipcRenderer.invoke("biometric", { + action: BiometricAction.EnableLinuxV2, + } satisfies BiometricMessage), + isLinuxV2BiometricsEnabled: (): Promise => + 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,