1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-02 17:53:41 +00:00

Merge branch 'main' into auth/pm-26209/bugfix-desktop-error-on-auth-request-approval

This commit is contained in:
rr-bw
2025-11-01 23:35:53 -07:00
23 changed files with 455 additions and 24 deletions

View File

@@ -232,7 +232,7 @@
<input formControlName="enableAutoTotpCopy" bitCheckbox id="totp" type="checkbox" />
<bit-label for="totp">{{ "enableAutoTotpCopy" | i18n }}</bit-label>
</bit-form-control>
<bit-form-field>
<bit-form-field [disableMargin]="isDefaultUriMatchDisabledByPolicy">
<bit-label for="clearClipboard">{{ "clearClipboard" | i18n }}</bit-label>
<bit-select
formControlName="clearClipboard"
@@ -250,7 +250,7 @@
{{ "clearClipboardDesc" | i18n }}
</bit-hint>
</bit-form-field>
<bit-form-field disableMargin>
<bit-form-field disableMargin *ngIf="!isDefaultUriMatchDisabledByPolicy">
<bit-label for="defaultUriMatch">{{ "defaultUriMatchDetection" | i18n }}</bit-label>
<bit-select
formControlName="defaultUriMatch"
@@ -265,9 +265,6 @@
[disabled]="option.disabled"
></bit-option>
</bit-select>
<bit-hint *ngIf="isDefaultUriMatchDisabledByPolicy">
{{ "settingDisabledByPolicy" | i18n }}
</bit-hint>
<bit-hint *ngIf="getMatchHints() as hints">
{{ hints[0] | i18n }}
<ng-container *ngIf="hints.length > 1">

View File

@@ -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<void>;
abstract isWindowsV2BiometricsEnabled(): Promise<boolean>;
/* Enables the v2 biometrics re-write. This will stay enabled until the application is restarted. */
abstract enableLinuxV2Biometrics(): Promise<void>;
abstract isLinuxV2BiometricsEnabled(): Promise<boolean>;
}

View File

@@ -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;
}

View File

@@ -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<boolean> {
return this.windowsV2BiometricsEnabled;
}
async enableLinuxV2Biometrics(): Promise<void> {
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<boolean> {
return this.linuxV2BiometricsEnabled;
}
}

View File

@@ -1 +1,2 @@
export { default as WindowsBiometricsSystem } from "./os-biometrics-windows.service";
export { default as LinuxBiometricsSystem } from "./os-biometrics-linux.service";

View File

@@ -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);
});
});

View File

@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<action id="com.bitwarden.Bitwarden.unlock">
<description>Unlock Bitwarden</description>
<message>Authenticate to unlock Bitwarden</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
</policyconfig>`;
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<void> {
await biometrics_v2.provideKey(
this.biometricsSystem,
userId,
Buffer.from(key.toEncoded().buffer),
);
}
async deleteBiometricKey(userId: UserId): Promise<void> {
await biometrics_v2.unenroll(this.biometricsSystem, userId);
}
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
const result = await biometrics_v2.unlock(this.biometricsSystem, userId, Buffer.from(""));
return result ? new SymmetricCryptoKey(Uint8Array.from(result)) : null;
}
async authenticateBiometric(): Promise<boolean> {
return await biometrics_v2.authenticate(
this.biometricsSystem,
Buffer.from(""),
"Authenticate to unlock",
);
}
async supportsBiometrics(): Promise<boolean> {
// 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<boolean> {
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<boolean> {
// 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<void> {
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<BiometricsStatus> {
return (await biometrics_v2.unlockAvailable(this.biometricsSystem, userId))
? BiometricsStatus.Available
: BiometricsStatus.UnlockNeeded;
}
async enrollPersistent(userId: UserId, key: SymmetricCryptoKey): Promise<void> {}
async hasPersistentKey(userId: UserId): Promise<boolean> {
return false;
}
}

View File

@@ -84,4 +84,12 @@ export class RendererBiometricsService extends DesktopBiometricsService {
async isWindowsV2BiometricsEnabled(): Promise<boolean> {
return await ipc.keyManagement.biometric.isWindowsV2BiometricsEnabled();
}
async enableLinuxV2Biometrics(): Promise<void> {
return await ipc.keyManagement.biometric.enableLinuxV2Biometrics();
}
async isLinuxV2BiometricsEnabled(): Promise<boolean> {
return await ipc.keyManagement.biometric.isLinuxV2BiometricsEnabled();
}
}

View File

@@ -69,6 +69,14 @@ const biometric = {
ipcRenderer.invoke("biometric", {
action: BiometricAction.IsWindowsV2Enabled,
} satisfies BiometricMessage),
enableLinuxV2Biometrics: (): Promise<void> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.EnableLinuxV2,
} satisfies BiometricMessage),
isLinuxV2BiometricsEnabled: (): Promise<boolean> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.IsLinuxV2Enabled,
} satisfies BiometricMessage),
};
export default {

View File

@@ -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) {

View File

@@ -19,6 +19,9 @@ export enum BiometricAction {
EnableWindowsV2 = "enableWindowsV2",
IsWindowsV2Enabled = "isWindowsV2Enabled",
EnableLinuxV2 = "enableLinuxV2",
IsLinuxV2Enabled = "isLinuxV2Enabled",
}
export type BiometricMessage =

View File

@@ -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) {

View File

@@ -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,

View File

@@ -0,0 +1,22 @@
<bit-callout title="{{ 'prerequisite' | i18n }}">
{{ "requireSsoPolicyReq" | i18n }}
</bit-callout>
<bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
<bit-label>{{ "turnOn" | i18n }}</bit-label>
</bit-form-control>
<div [formGroup]="data">
<bit-form-field class="tw-flex-auto">
<bit-label>{{ "uriMatchDetectionOptionsLabel" | i18n }}</bit-label>
<bit-select formControlName="uriMatchDetection" id="uriMatchDetection">
<bit-option
*ngFor="let o of uriMatchOptions"
[label]="o.label"
[value]="o.value"
[disabled]="o.disabled"
></bit-option>
</bit-select>
</bit-form-field>
</div>

View File

@@ -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<UriMatchStrategySetting>(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<PolicyRequest> {
const request = await super.buildRequest();
if (request.data?.uriMatchDetection == null) {
throw new Error(this.i18nService.t("invalidUriMatchDefaultPolicySetting"));
}
return request;
}
}

View File

@@ -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(),
];

View File

@@ -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 members 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."
},

View File

@@ -2,7 +2,7 @@
<span bitTypography="h6" class="tw-flex tw-text-main">{{ title }}</span>
<div class="tw-flex tw-items-baseline tw-gap-2">
@if (iconClass) {
<i class="bwi {{ iconClass }} tw-text-muted" aria-hidden="true"></i>
<i class="bwi {{ iconClass }} {{ iconColorClass }}" aria-hidden="true"></i>
}
<span bitTypography="h3">{{ cardMetrics }}</span>
</div>

View File

@@ -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.
*/

View File

@@ -45,10 +45,19 @@
<li class="tw-col-span-1">
<dirt-activity-card
[title]="'applicationsNeedingReview' | i18n"
[cardMetrics]="'newApplicationsWithCount' | i18n: newApplicationsCount"
[metricDescription]="'newApplicationsDescription' | i18n"
[iconClass]="'bwi-exclamation-triangle'"
[buttonText]="'reviewNow' | i18n"
[cardMetrics]="
isAllCaughtUp
? ('allCaughtUp' | i18n)
: ('newApplicationsWithCount' | i18n: newApplicationsCount)
"
[metricDescription]="
isAllCaughtUp
? ('noNewApplicationsToReviewAtThisTime' | i18n)
: ('newApplicationsDescription' | i18n)
"
[iconClass]="isAllCaughtUp ? 'bwi-check-circle' : 'bwi-exclamation-triangle'"
[iconColorClass]="isAllCaughtUp ? 'tw-text-success' : 'tw-text-muted'"
[buttonText]="isAllCaughtUp ? '' : ('reviewNow' | i18n)"
[buttonType]="'primary'"
(buttonClick)="onReviewNewApplications()"
>

View File

@@ -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.

View File

@@ -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;
}),

View File

@@ -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,