1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-20 17:33:28 +00:00

[PM-26057] Enforce session timeout policy (#17424)

* enforce session timeout policy

* better angular validation

* lint fix

* missing switch break

* fallback when timeout not supported with highest available timeout

* failing unit tests

* incorrect policy message

* vault timeout type adjustments

* fallback to "on browser refresh" for browser, when policy is set to "on system locked", but not available (Safari)

* docs, naming improvements

* fallback for current user session timeout to "on refresh", when policy is set to "on system locked", but not available.

* don't display policy message when the policy does not affect available timeout options

* 8 hours default when changing from non-numeric timeout to Custom.

* failing unit test

* missing locales, changing functions access to private, docs

* removal of redundant magic number

* missing await

* await once for available timeout options

* adjusted messaging

* unit test coverage

* vault timeout numeric module exports

* unit test coverage
This commit is contained in:
Maciej Zieniuk
2025-12-05 14:55:59 +01:00
committed by GitHub
parent c036ffd775
commit bbea11388a
48 changed files with 3344 additions and 569 deletions

View File

@@ -1,48 +0,0 @@
import { defer, from, map, Observable } from "rxjs";
import {
VaultTimeout,
VaultTimeoutOption,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SessionTimeoutSettingsComponentService } from "@bitwarden/key-management-ui";
export class DesktopSessionTimeoutSettingsComponentService
implements SessionTimeoutSettingsComponentService
{
availableTimeoutOptions$: Observable<VaultTimeoutOption[]> = defer(() =>
from(ipc.platform.powermonitor.isLockMonitorAvailable()).pipe(
map((isLockMonitorAvailable) => {
const options: VaultTimeoutOption[] = [
{ name: this.i18nService.t("oneMinute"), value: 1 },
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
{ name: this.i18nService.t("oneHour"), value: 60 },
{ name: this.i18nService.t("fourHours"), value: 240 },
{ name: this.i18nService.t("onIdle"), value: VaultTimeoutStringType.OnIdle },
{ name: this.i18nService.t("onSleep"), value: VaultTimeoutStringType.OnSleep },
];
if (isLockMonitorAvailable) {
options.push({
name: this.i18nService.t("onLocked"),
value: VaultTimeoutStringType.OnLocked,
});
}
options.push(
{ name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart },
{ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never },
);
return options;
}),
),
);
constructor(private readonly i18nService: I18nService) {}
onTimeoutSave(_: VaultTimeout): void {}
}

View File

@@ -0,0 +1,125 @@
import {
VaultTimeoutNumberType,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { DesktopSessionTimeoutTypeService } from "./desktop-session-timeout-type.service";
describe("DesktopSessionTimeoutTypeService", () => {
let service: DesktopSessionTimeoutTypeService;
let mockIsLockMonitorAvailable: jest.Mock;
beforeEach(() => {
mockIsLockMonitorAvailable = jest.fn();
(global as any).ipc = {
platform: {
powermonitor: {
isLockMonitorAvailable: mockIsLockMonitorAvailable,
},
},
};
service = new DesktopSessionTimeoutTypeService();
});
describe("isAvailable", () => {
it("should return false for Immediately", async () => {
const result = await service.isAvailable(VaultTimeoutNumberType.Immediately);
expect(result).toBe(false);
});
it.each([
VaultTimeoutStringType.OnIdle,
VaultTimeoutStringType.OnSleep,
VaultTimeoutStringType.OnRestart,
VaultTimeoutStringType.Never,
VaultTimeoutStringType.Custom,
])("should return true for always available type: %s", async (timeoutType) => {
const result = await service.isAvailable(timeoutType);
expect(result).toBe(true);
});
it.each([VaultTimeoutNumberType.OnMinute, VaultTimeoutNumberType.EightHours])(
"should return true for numeric timeout type: %s",
async (timeoutType) => {
const result = await service.isAvailable(timeoutType);
expect(result).toBe(true);
},
);
describe("OnLocked availability", () => {
it("should return true when lock monitor is available", async () => {
mockIsLockMonitorAvailable.mockResolvedValue(true);
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
expect(result).toBe(true);
expect(mockIsLockMonitorAvailable).toHaveBeenCalled();
});
it("should return false when lock monitor is not available", async () => {
mockIsLockMonitorAvailable.mockResolvedValue(false);
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
expect(result).toBe(false);
expect(mockIsLockMonitorAvailable).toHaveBeenCalled();
});
});
});
describe("getOrPromoteToAvailable", () => {
it.each([
VaultTimeoutNumberType.OnMinute,
VaultTimeoutStringType.OnIdle,
VaultTimeoutStringType.OnSleep,
VaultTimeoutStringType.OnRestart,
VaultTimeoutStringType.OnLocked,
VaultTimeoutStringType.Never,
VaultTimeoutStringType.Custom,
])("should return the original type when it is available: %s", async (timeoutType) => {
jest.spyOn(service, "isAvailable").mockResolvedValue(true);
const result = await service.getOrPromoteToAvailable(timeoutType);
expect(result).toBe(timeoutType);
expect(service.isAvailable).toHaveBeenCalledWith(timeoutType);
});
it("should return OnMinute when Immediately is not available", async () => {
jest.spyOn(service, "isAvailable").mockResolvedValue(false);
const result = await service.getOrPromoteToAvailable(VaultTimeoutNumberType.Immediately);
expect(result).toBe(VaultTimeoutNumberType.OnMinute);
expect(service.isAvailable).toHaveBeenCalledWith(VaultTimeoutNumberType.Immediately);
});
it("should return OnSleep when OnLocked is not available", async () => {
jest.spyOn(service, "isAvailable").mockResolvedValue(false);
const result = await service.getOrPromoteToAvailable(VaultTimeoutStringType.OnLocked);
expect(result).toBe(VaultTimeoutStringType.OnSleep);
expect(service.isAvailable).toHaveBeenCalledWith(VaultTimeoutStringType.OnLocked);
});
it.each([
VaultTimeoutStringType.OnIdle,
VaultTimeoutStringType.OnSleep,
VaultTimeoutNumberType.OnMinute,
5,
])("should return OnRestart when type is not available: %s", async (timeoutType) => {
jest.spyOn(service, "isAvailable").mockResolvedValue(false);
const result = await service.getOrPromoteToAvailable(timeoutType);
expect(result).toBe(VaultTimeoutStringType.OnRestart);
expect(service.isAvailable).toHaveBeenCalledWith(timeoutType);
});
});
});

View File

@@ -0,0 +1,46 @@
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
import {
isVaultTimeoutTypeNumeric,
VaultTimeout,
VaultTimeoutNumberType,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
export class DesktopSessionTimeoutTypeService implements SessionTimeoutTypeService {
async isAvailable(type: VaultTimeout): Promise<boolean> {
switch (type) {
case VaultTimeoutNumberType.Immediately:
return false;
case VaultTimeoutStringType.OnIdle:
case VaultTimeoutStringType.OnSleep:
case VaultTimeoutStringType.OnRestart:
case VaultTimeoutStringType.Never:
case VaultTimeoutStringType.Custom:
return true;
case VaultTimeoutStringType.OnLocked:
return await ipc.platform.powermonitor.isLockMonitorAvailable();
default:
if (isVaultTimeoutTypeNumeric(type)) {
return true;
}
break;
}
return false;
}
async getOrPromoteToAvailable(type: VaultTimeout): Promise<VaultTimeout> {
const available = await this.isAvailable(type);
if (!available) {
switch (type) {
case VaultTimeoutNumberType.Immediately:
return VaultTimeoutNumberType.OnMinute;
case VaultTimeoutStringType.OnLocked:
return VaultTimeoutStringType.OnSleep;
default:
return VaultTimeoutStringType.OnRestart;
}
}
return type;
}
}