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:
@@ -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">
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as WindowsBiometricsSystem } from "./os-biometrics-windows.service";
|
||||
export { default as LinuxBiometricsSystem } from "./os-biometrics-linux.service";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -19,6 +19,9 @@ export enum BiometricAction {
|
||||
|
||||
EnableWindowsV2 = "enableWindowsV2",
|
||||
IsWindowsV2Enabled = "isWindowsV2Enabled",
|
||||
|
||||
EnableLinuxV2 = "enableLinuxV2",
|
||||
IsLinuxV2Enabled = "isLinuxV2Enabled",
|
||||
}
|
||||
|
||||
export type BiometricMessage =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
];
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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()"
|
||||
>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user