mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge 4c613a2b6f into d32365fbba
This commit is contained in:
commit
921000801a
@ -5881,5 +5881,49 @@
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToImmediately": {
|
||||
"message": "Your organization has set the default session timeout to Immediately."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On browser restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On browser restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,12 +86,12 @@
|
||||
</bit-section-header>
|
||||
|
||||
<bit-card>
|
||||
<bit-session-timeout-input
|
||||
<bit-session-timeout-input-legacy
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</bit-session-timeout-input>
|
||||
</bit-session-timeout-input-legacy>
|
||||
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label for="vaultTimeoutAction">{{ "vaultTimeoutAction1" | i18n }}</bit-label>
|
||||
|
||||
@ -70,7 +70,7 @@ import {
|
||||
BiometricsStatus,
|
||||
} from "@bitwarden/key-management";
|
||||
import {
|
||||
SessionTimeoutInputComponent,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
SessionTimeoutSettingsComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
|
||||
@ -109,7 +109,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||
SessionTimeoutSettingsComponent,
|
||||
SpotlightComponent,
|
||||
TypographyModule,
|
||||
SessionTimeoutInputComponent,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
],
|
||||
})
|
||||
export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -297,6 +297,7 @@ import { SafariApp } from "../browser/safariApp";
|
||||
import { PhishingDataService } from "../dirt/phishing-detection/services/phishing-data.service";
|
||||
import { PhishingDetectionService } from "../dirt/phishing-detection/services/phishing-detection.service";
|
||||
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
|
||||
import { BrowserSessionTimeoutTypeService } from "../key-management/session-timeout/services/browser-session-timeout-type.service";
|
||||
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
|
||||
import { BrowserActionsService } from "../platform/actions/browser-actions.service";
|
||||
import { DefaultBadgeBrowserApi } from "../platform/badge/badge-browser-api";
|
||||
@ -738,6 +739,10 @@ export default class MainBackground {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
const sessionTimeoutTypeService = new BrowserSessionTimeoutTypeService(
|
||||
this.platformUtilsService,
|
||||
);
|
||||
|
||||
this.vaultTimeoutSettingsService = new DefaultVaultTimeoutSettingsService(
|
||||
this.accountService,
|
||||
pinStateService,
|
||||
@ -749,6 +754,7 @@ export default class MainBackground {
|
||||
this.stateProvider,
|
||||
this.logService,
|
||||
VaultTimeoutStringType.OnRestart, // default vault timeout
|
||||
sessionTimeoutTypeService,
|
||||
);
|
||||
|
||||
this.apiService = new ApiService(
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import { BrowserSessionTimeoutSettingsComponentService } from "./browser-session-timeout-settings-component.service";
|
||||
|
||||
describe("BrowserSessionTimeoutSettingsComponentService", () => {
|
||||
let service: BrowserSessionTimeoutSettingsComponentService;
|
||||
let mockI18nService: jest.Mocked<I18nService>;
|
||||
let mockSessionTimeoutTypeService: jest.Mocked<SessionTimeoutTypeService>;
|
||||
let mockPolicyService: jest.Mocked<PolicyService>;
|
||||
let mockMessagingService: jest.Mocked<MessagingService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockSessionTimeoutTypeService = mock<SessionTimeoutTypeService>();
|
||||
mockPolicyService = mock<PolicyService>();
|
||||
mockMessagingService = mock<MessagingService>();
|
||||
|
||||
service = new BrowserSessionTimeoutSettingsComponentService(
|
||||
mockI18nService,
|
||||
mockSessionTimeoutTypeService,
|
||||
mockPolicyService,
|
||||
mockMessagingService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("onTimeoutSave", () => {
|
||||
it("should call messagingService.send with 'bgReseedStorage' when timeout is Never", () => {
|
||||
service.onTimeoutSave(VaultTimeoutStringType.Never);
|
||||
|
||||
expect(mockMessagingService.send).toHaveBeenCalledWith("bgReseedStorage");
|
||||
});
|
||||
|
||||
it.each([
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.Custom,
|
||||
])("should not call messagingService.send when timeout is %s", (timeoutValue) => {
|
||||
service.onTimeoutSave(timeoutValue);
|
||||
|
||||
expect(mockMessagingService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,56 +1,24 @@
|
||||
import { defer, Observable, of } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SessionTimeoutSettingsComponentService } from "@bitwarden/key-management-ui";
|
||||
|
||||
export class BrowserSessionTimeoutSettingsComponentService
|
||||
implements SessionTimeoutSettingsComponentService
|
||||
{
|
||||
availableTimeoutOptions$: Observable<VaultTimeoutOption[]> = defer(() => {
|
||||
const options: VaultTimeoutOption[] = [
|
||||
{ name: this.i18nService.t("immediately"), value: 0 },
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
const showOnLocked =
|
||||
!this.platformUtilsService.isFirefox() &&
|
||||
!this.platformUtilsService.isSafari() &&
|
||||
!(this.platformUtilsService.isOpera() && navigator.platform === "MacIntel");
|
||||
|
||||
if (showOnLocked) {
|
||||
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 of(options);
|
||||
});
|
||||
|
||||
export class BrowserSessionTimeoutSettingsComponentService extends SessionTimeoutSettingsComponentService {
|
||||
constructor(
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
sessionTimeoutTypeService: SessionTimeoutTypeService,
|
||||
policyService: PolicyService,
|
||||
private readonly messagingService: MessagingService,
|
||||
) {}
|
||||
) {
|
||||
super(i18nService, sessionTimeoutTypeService, policyService);
|
||||
}
|
||||
|
||||
onTimeoutSave(timeout: VaultTimeout): void {
|
||||
override onTimeoutSave(timeout: VaultTimeout): void {
|
||||
if (timeout === VaultTimeoutStringType.Never) {
|
||||
this.messagingService.send("bgReseedStorage");
|
||||
}
|
||||
|
||||
@ -0,0 +1,139 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { BrowserSessionTimeoutTypeService } from "./browser-session-timeout-type.service";
|
||||
|
||||
describe("BrowserSessionTimeoutTypeService", () => {
|
||||
let service: BrowserSessionTimeoutTypeService;
|
||||
let mockPlatformUtilsService: jest.Mocked<PlatformUtilsService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
service = new BrowserSessionTimeoutTypeService(mockPlatformUtilsService);
|
||||
});
|
||||
|
||||
describe("isAvailable", () => {
|
||||
it.each([
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
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", () => {
|
||||
const mockNavigatorPlatform = (platform: string) => {
|
||||
Object.defineProperty(navigator, "platform", {
|
||||
value: platform,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockNavigatorPlatform("Linux x86_64");
|
||||
mockPlatformUtilsService.isFirefox.mockReturnValue(false);
|
||||
mockPlatformUtilsService.isSafari.mockReturnValue(false);
|
||||
mockPlatformUtilsService.isOpera.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should return true when not Firefox, Safari, or Opera on Mac", async () => {
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when Opera on non-Mac platform", async () => {
|
||||
mockNavigatorPlatform("Win32");
|
||||
mockPlatformUtilsService.isOpera.mockReturnValue(true);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when Opera on Mac", async () => {
|
||||
mockNavigatorPlatform("MacIntel");
|
||||
mockPlatformUtilsService.isOpera.mockReturnValue(true);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when Firefox", async () => {
|
||||
mockPlatformUtilsService.isFirefox.mockReturnValue(true);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when Safari", async () => {
|
||||
mockPlatformUtilsService.isSafari.mockReturnValue(true);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([VaultTimeoutStringType.OnIdle, VaultTimeoutStringType.OnSleep])(
|
||||
"should return false for unavailable timeout type: %s",
|
||||
async (timeoutType) => {
|
||||
const result = await service.isAvailable(timeoutType);
|
||||
|
||||
expect(result).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("getOrPromoteToAvailable", () => {
|
||||
it.each([
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
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.each([
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,43 @@
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
isVaultTimeoutTypeNumeric,
|
||||
VaultTimeout,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
export class BrowserSessionTimeoutTypeService implements SessionTimeoutTypeService {
|
||||
constructor(private readonly platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async isAvailable(type: VaultTimeout): Promise<boolean> {
|
||||
switch (type) {
|
||||
case VaultTimeoutNumberType.Immediately:
|
||||
case VaultTimeoutStringType.OnRestart:
|
||||
case VaultTimeoutStringType.Never:
|
||||
case VaultTimeoutStringType.Custom:
|
||||
return true;
|
||||
case VaultTimeoutStringType.OnLocked:
|
||||
return (
|
||||
!this.platformUtilsService.isFirefox() &&
|
||||
!this.platformUtilsService.isSafari() &&
|
||||
!(this.platformUtilsService.isOpera() && navigator.platform === "MacIntel")
|
||||
);
|
||||
default:
|
||||
if (isVaultTimeoutTypeNumeric(type)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async getOrPromoteToAvailable(type: VaultTimeout): Promise<VaultTimeout> {
|
||||
const available = await this.isAvailable(type);
|
||||
if (!available) {
|
||||
return VaultTimeoutStringType.OnRestart;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,7 @@ import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeoutService,
|
||||
VaultTimeoutStringType,
|
||||
@ -170,6 +171,7 @@ import { InlineMenuFieldQualificationService } from "../../autofill/services/inl
|
||||
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
|
||||
import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service";
|
||||
import { BrowserSessionTimeoutSettingsComponentService } from "../../key-management/session-timeout/services/browser-session-timeout-settings-component.service";
|
||||
import { BrowserSessionTimeoutTypeService } from "../../key-management/session-timeout/services/browser-session-timeout-type.service";
|
||||
import { ForegroundVaultTimeoutService } from "../../key-management/vault-timeout/foreground-vault-timeout.service";
|
||||
import { BrowserActionsService } from "../../platform/actions/browser-actions.service";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
@ -723,10 +725,20 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ExtensionNewDeviceVerificationComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutTypeService,
|
||||
useClass: BrowserSessionTimeoutTypeService,
|
||||
deps: [PlatformUtilsService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutSettingsComponentService,
|
||||
useClass: BrowserSessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsService, MessagingServiceAbstraction],
|
||||
deps: [
|
||||
I18nServiceAbstraction,
|
||||
SessionTimeoutTypeService,
|
||||
PolicyService,
|
||||
MessagingServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
|
||||
export class CliSessionTimeoutTypeService implements SessionTimeoutTypeService {
|
||||
async isAvailable(timeout: VaultTimeout): Promise<boolean> {
|
||||
return timeout === VaultTimeoutStringType.Never;
|
||||
}
|
||||
|
||||
async getOrPromoteToAvailable(_: VaultTimeout): Promise<VaultTimeout> {
|
||||
return VaultTimeoutStringType.Never;
|
||||
}
|
||||
}
|
||||
@ -211,6 +211,7 @@ import {
|
||||
|
||||
import { CliBiometricsService } from "../key-management/cli-biometrics-service";
|
||||
import { CliProcessReloadService } from "../key-management/cli-process-reload.service";
|
||||
import { CliSessionTimeoutTypeService } from "../key-management/session-timeout/services/cli-session-timeout-type.service";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service";
|
||||
import { CliSdkLoadService } from "../platform/services/cli-sdk-load.service";
|
||||
@ -529,6 +530,8 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
const sessionTimeoutTypeService = new CliSessionTimeoutTypeService();
|
||||
|
||||
this.vaultTimeoutSettingsService = new DefaultVaultTimeoutSettingsService(
|
||||
this.accountService,
|
||||
pinStateService,
|
||||
@ -540,6 +543,7 @@ export class ServiceContainer {
|
||||
this.stateProvider,
|
||||
this.logService,
|
||||
VaultTimeoutStringType.Never, // default vault timeout
|
||||
sessionTimeoutTypeService,
|
||||
);
|
||||
|
||||
const refreshAccessTokenErrorCallback = () => {
|
||||
|
||||
@ -44,12 +44,12 @@
|
||||
<h2 bitTypography="h6">{{ "vaultTimeoutHeader" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
|
||||
<bit-session-timeout-input
|
||||
<bit-session-timeout-input-legacy
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</bit-session-timeout-input>
|
||||
</bit-session-timeout-input-legacy>
|
||||
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label for="vaultTimeoutAction">{{
|
||||
|
||||
@ -55,7 +55,7 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
|
||||
import {
|
||||
SessionTimeoutInputComponent,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
SessionTimeoutSettingsComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
@ -97,7 +97,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
|
||||
SectionHeaderComponent,
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
SessionTimeoutInputComponent,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
SessionTimeoutSettingsComponent,
|
||||
PermitCipherDetailsPopoverComponent,
|
||||
PremiumBadgeComponent,
|
||||
|
||||
@ -62,6 +62,7 @@ import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypt
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
@ -128,7 +129,7 @@ import { DesktopBiometricsService } from "../../key-management/biometrics/deskto
|
||||
import { RendererBiometricsService } from "../../key-management/biometrics/renderer-biometrics.service";
|
||||
import { ElectronKeyService } from "../../key-management/electron-key.service";
|
||||
import { DesktopLockComponentService } from "../../key-management/lock/services/desktop-lock-component.service";
|
||||
import { DesktopSessionTimeoutSettingsComponentService } from "../../key-management/session-timeout/services/desktop-session-timeout-settings-component.service";
|
||||
import { DesktopSessionTimeoutTypeService } from "../../key-management/session-timeout/services/desktop-session-timeout-type.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
|
||||
@ -484,10 +485,15 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DesktopAutotypeDefaultSettingPolicy,
|
||||
deps: [AccountServiceAbstraction, AuthServiceAbstraction, InternalPolicyService, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutTypeService,
|
||||
useClass: DesktopSessionTimeoutTypeService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutSettingsComponentService,
|
||||
useClass: DesktopSessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction],
|
||||
useClass: SessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction, SessionTimeoutTypeService, PolicyServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -4255,5 +4255,46 @@
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,6 +69,7 @@ import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-managemen
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutStringType,
|
||||
@ -124,7 +125,6 @@ import {
|
||||
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service";
|
||||
import { WebSessionTimeoutSettingsComponentService } from "@bitwarden/web-vault/app/key-management/session-timeout/services/web-session-timeout-settings-component.service";
|
||||
import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/vault/services/web-premium-upgrade-prompt.service";
|
||||
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
@ -149,6 +149,7 @@ import { WebFileDownloadService } from "../core/web-file-download.service";
|
||||
import { UserKeyRotationService } from "../key-management/key-rotation/user-key-rotation.service";
|
||||
import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service";
|
||||
import { WebProcessReloadService } from "../key-management/services/web-process-reload.service";
|
||||
import { WebSessionTimeoutTypeService } from "../key-management/session-timeout/services/web-session-timeout-type.service";
|
||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
import { WebIpcService } from "../platform/ipc/web-ipc.service";
|
||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||
@ -469,10 +470,15 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebSystemService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutTypeService,
|
||||
useClass: WebSessionTimeoutTypeService,
|
||||
deps: [PlatformUtilsService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutSettingsComponentService,
|
||||
useClass: WebSessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsService],
|
||||
useClass: SessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction, SessionTimeoutTypeService, PolicyService],
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { defer, Observable, of } from "rxjs";
|
||||
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SessionTimeoutSettingsComponentService } from "@bitwarden/key-management-ui";
|
||||
|
||||
export class WebSessionTimeoutSettingsComponentService
|
||||
implements SessionTimeoutSettingsComponentService
|
||||
{
|
||||
availableTimeoutOptions$: Observable<VaultTimeoutOption[]> = defer(() => {
|
||||
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("onRefresh"), value: VaultTimeoutStringType.OnRestart },
|
||||
];
|
||||
|
||||
if (this.platformUtilsService.isDev()) {
|
||||
options.push({ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never });
|
||||
}
|
||||
|
||||
return of(options);
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
|
||||
onTimeoutSave(_: VaultTimeout): void {}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { WebSessionTimeoutTypeService } from "./web-session-timeout-type.service";
|
||||
|
||||
describe("WebSessionTimeoutTypeService", () => {
|
||||
let service: WebSessionTimeoutTypeService;
|
||||
let mockPlatformUtilsService: jest.Mocked<PlatformUtilsService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
service = new WebSessionTimeoutTypeService(mockPlatformUtilsService);
|
||||
});
|
||||
|
||||
describe("isAvailable", () => {
|
||||
it("should return false for Immediately", async () => {
|
||||
const result = await service.isAvailable(VaultTimeoutNumberType.Immediately);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it.each([VaultTimeoutStringType.OnRestart, 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);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
])("should return false for unavailable timeout type: %s", async (timeoutType) => {
|
||||
const result = await service.isAvailable(timeoutType);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
describe("Never availability", () => {
|
||||
it("should return true when in dev mode", async () => {
|
||||
mockPlatformUtilsService.isDev.mockReturnValue(true);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.Never);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPlatformUtilsService.isDev).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when not in dev mode", async () => {
|
||||
mockPlatformUtilsService.isDev.mockReturnValue(false);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.Never);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockPlatformUtilsService.isDev).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrPromoteToAvailable", () => {
|
||||
it.each([
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
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.each([
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.Never,
|
||||
])("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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,44 @@
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
isVaultTimeoutTypeNumeric,
|
||||
VaultTimeout,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
export class WebSessionTimeoutTypeService implements SessionTimeoutTypeService {
|
||||
constructor(private readonly platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async isAvailable(type: VaultTimeout): Promise<boolean> {
|
||||
switch (type) {
|
||||
case VaultTimeoutNumberType.Immediately:
|
||||
return false;
|
||||
case VaultTimeoutStringType.OnRestart:
|
||||
case VaultTimeoutStringType.Custom:
|
||||
return true;
|
||||
case VaultTimeoutStringType.Never:
|
||||
return this.platformUtilsService.isDev();
|
||||
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;
|
||||
default:
|
||||
return VaultTimeoutStringType.OnRestart;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@ -17,12 +17,12 @@
|
||||
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
||||
</span>
|
||||
</bit-callout>
|
||||
<bit-session-timeout-input
|
||||
<bit-session-timeout-input-legacy
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</bit-session-timeout-input>
|
||||
</bit-session-timeout-input-legacy>
|
||||
<ng-container *ngIf="availableVaultTimeoutActions$ | async as availableVaultTimeoutActions">
|
||||
<bit-radio-group
|
||||
formControlName="vaultTimeoutAction"
|
||||
|
||||
@ -33,7 +33,7 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { SessionTimeoutInputComponent } from "@bitwarden/key-management-ui";
|
||||
import { SessionTimeoutInputLegacyComponent } from "@bitwarden/key-management-ui";
|
||||
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
@ -52,8 +52,8 @@ import { SharedModule } from "../shared";
|
||||
imports: [
|
||||
SharedModule,
|
||||
HeaderModule,
|
||||
SessionTimeoutInputComponent,
|
||||
PermitCipherDetailsPopoverComponent,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
],
|
||||
})
|
||||
export class PreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -12214,5 +12214,43 @@
|
||||
},
|
||||
"userVerificationFailed": {
|
||||
"message": "User verification failed."
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On browser refresh."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On browser refresh"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,16 +7,16 @@ import { mock } from "jest-mock-extended";
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import {
|
||||
SessionTimeoutAction,
|
||||
SessionTimeoutType,
|
||||
} from "@bitwarden/common/key-management/session-timeout";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogRef, DialogService } from "@bitwarden/components";
|
||||
|
||||
import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component";
|
||||
import {
|
||||
SessionTimeoutAction,
|
||||
SessionTimeoutPolicyComponent,
|
||||
SessionTimeoutType,
|
||||
} from "./session-timeout.component";
|
||||
import { SessionTimeoutPolicyComponent } from "./session-timeout.component";
|
||||
|
||||
// Mock DialogRef, so we can mock "readonly closed" property.
|
||||
class MockDialogRef extends DialogRef {
|
||||
|
||||
@ -10,26 +10,22 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
MaximumSessionTimeoutPolicyData,
|
||||
SessionTimeoutAction,
|
||||
SessionTimeoutType,
|
||||
} from "@bitwarden/common/key-management/session-timeout";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
BasePolicyEditDefinition,
|
||||
BasePolicyEditComponent,
|
||||
BasePolicyEditDefinition,
|
||||
} from "@bitwarden/web-vault/app/admin-console/organizations/policies";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component";
|
||||
|
||||
export type SessionTimeoutAction = null | "lock" | "logOut";
|
||||
export type SessionTimeoutType =
|
||||
| null
|
||||
| "never"
|
||||
| "onAppRestart"
|
||||
| "onSystemLock"
|
||||
| "immediately"
|
||||
| "custom";
|
||||
|
||||
export class SessionTimeoutPolicy extends BasePolicyEditDefinition {
|
||||
name = "sessionTimeoutPolicyTitle";
|
||||
description = "sessionTimeoutPolicyDescription";
|
||||
@ -50,9 +46,6 @@ export class SessionTimeoutPolicyComponent
|
||||
extends BasePolicyEditComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private destroy$ = new Subject<void>();
|
||||
private lastConfirmedType$ = new BehaviorSubject<SessionTimeoutType>(null);
|
||||
|
||||
actionOptions: { name: string; value: SessionTimeoutAction }[];
|
||||
typeOptions: { name: string; value: SessionTimeoutType }[];
|
||||
data = this.formBuilder.group({
|
||||
@ -74,6 +67,9 @@ export class SessionTimeoutPolicyComponent
|
||||
action: new FormControl<SessionTimeoutAction>(null),
|
||||
});
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private lastConfirmedType$ = new BehaviorSubject<SessionTimeoutType>(null);
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
@ -123,12 +119,10 @@ export class SessionTimeoutPolicyComponent
|
||||
}
|
||||
|
||||
protected override loadData() {
|
||||
const minutes: number | null = this.policyResponse?.data?.minutes ?? null;
|
||||
const action: SessionTimeoutAction =
|
||||
this.policyResponse?.data?.action ?? (null satisfies SessionTimeoutAction);
|
||||
const minutes: number | null = this.policyData?.minutes ?? null;
|
||||
const action: SessionTimeoutAction = this.policyData?.action ?? null;
|
||||
// For backward compatibility, the "type" field might not exist, hence we initialize it based on the presence of "minutes"
|
||||
const type: SessionTimeoutType =
|
||||
this.policyResponse?.data?.type ?? ((minutes ? "custom" : null) satisfies SessionTimeoutType);
|
||||
const type: SessionTimeoutType = this.policyData?.type ?? (minutes ? "custom" : null);
|
||||
|
||||
this.updateFormControls(type);
|
||||
this.data.patchValue({
|
||||
@ -165,7 +159,11 @@ export class SessionTimeoutPolicyComponent
|
||||
type,
|
||||
minutes,
|
||||
action: this.data.value.action,
|
||||
};
|
||||
} satisfies MaximumSessionTimeoutPolicyData;
|
||||
}
|
||||
|
||||
private get policyData(): MaximumSessionTimeoutPolicyData | null {
|
||||
return this.policyResponse?.data ?? null;
|
||||
}
|
||||
|
||||
private async confirmTypeChange(newType: SessionTimeoutType): Promise<boolean> {
|
||||
|
||||
@ -207,6 +207,7 @@ import {
|
||||
SendPasswordService,
|
||||
DefaultSendPasswordService,
|
||||
} from "@bitwarden/common/key-management/sends";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
DefaultVaultTimeoutService,
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@ -912,6 +913,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
LogService,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
SessionTimeoutTypeService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import { VaultTimeout } from "../../vault-timeout";
|
||||
|
||||
export abstract class SessionTimeoutTypeService {
|
||||
/**
|
||||
* Is provided timeout type available on this client type, OS ?
|
||||
* @param timeout the timeout type
|
||||
*/
|
||||
abstract isAvailable(timeout: VaultTimeout): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Returns the highest available and permissive timeout type, that is higher than or equals the provided timeout type.
|
||||
* @param timeout the provided timeout type
|
||||
*/
|
||||
abstract getOrPromoteToAvailable(timeout: VaultTimeout): Promise<VaultTimeout>;
|
||||
}
|
||||
3
libs/common/src/key-management/session-timeout/index.ts
Normal file
3
libs/common/src/key-management/session-timeout/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { SessionTimeoutTypeService } from "./abstractions/session-timeout-type.service";
|
||||
export { MaximumSessionTimeoutPolicyData } from "./types/maximum-session-timeout-policy.type";
|
||||
export { SessionTimeoutAction, SessionTimeoutType } from "./types/session-timeout.type";
|
||||
@ -0,0 +1,7 @@
|
||||
import { SessionTimeoutAction, SessionTimeoutType } from "./session-timeout.type";
|
||||
|
||||
export interface MaximumSessionTimeoutPolicyData {
|
||||
type?: SessionTimeoutType;
|
||||
minutes: number;
|
||||
action?: SessionTimeoutAction;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
export type SessionTimeoutAction = null | "lock" | "logOut";
|
||||
export type SessionTimeoutType =
|
||||
| null
|
||||
| "never"
|
||||
| "onAppRestart"
|
||||
| "onSystemLock"
|
||||
| "immediately"
|
||||
| "custom";
|
||||
@ -4,8 +4,9 @@ export { VaultTimeoutService } from "./abstractions/vault-timeout.service";
|
||||
export { VaultTimeoutService as DefaultVaultTimeoutService } from "./services/vault-timeout.service";
|
||||
export { VaultTimeoutAction } from "./enums/vault-timeout-action.enum";
|
||||
export {
|
||||
isVaultTimeoutTypeNumeric,
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "./types/vault-timeout.type";
|
||||
export { MaximumVaultTimeoutPolicyData } from "./types/maximum-vault-timeout-policy.type";
|
||||
|
||||
@ -21,9 +21,14 @@ import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PinStateServiceAbstraction } from "../../pin/pin-state.service.abstraction";
|
||||
import { SessionTimeoutTypeService } from "../../session-timeout";
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../abstractions/vault-timeout-settings.service";
|
||||
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "../types/vault-timeout.type";
|
||||
|
||||
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
|
||||
import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state";
|
||||
@ -40,9 +45,11 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
|
||||
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
|
||||
|
||||
const defaultVaultTimeout: VaultTimeout = 15; // default web vault timeout
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let logService: MockProxy<LogService>;
|
||||
let sessionTimeoutTypeService: MockProxy<SessionTimeoutTypeService>;
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
@ -67,8 +74,8 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
logService = mock<LogService>();
|
||||
sessionTimeoutTypeService = mock<SessionTimeoutTypeService>();
|
||||
|
||||
const defaultVaultTimeout: VaultTimeout = 15; // default web vault timeout
|
||||
vaultTimeoutSettingsService = createVaultTimeoutSettingsService(defaultVaultTimeout);
|
||||
|
||||
biometricStateService.biometricUnlockEnabled$ = of(false);
|
||||
@ -259,40 +266,276 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
// policy, vaultTimeout, expected
|
||||
[null, null, 15], // no policy, no vault timeout, falls back to default
|
||||
[30, 90, 30], // policy overrides vault timeout
|
||||
[30, 15, 15], // policy doesn't override vault timeout when it's within acceptable range
|
||||
[90, VaultTimeoutStringType.Never, 90], // policy overrides vault timeout when it's "never"
|
||||
[null, VaultTimeoutStringType.Never, VaultTimeoutStringType.Never], // no policy, persist "never" vault timeout
|
||||
[90, 0, 0], // policy doesn't override vault timeout when it's 0 (immediate)
|
||||
[null, 0, 0], // no policy, persist 0 (immediate) vault timeout
|
||||
[90, VaultTimeoutStringType.OnRestart, 90], // policy overrides vault timeout when it's "onRestart"
|
||||
[null, VaultTimeoutStringType.OnRestart, VaultTimeoutStringType.OnRestart], // no policy, persist "onRestart" vault timeout
|
||||
[90, VaultTimeoutStringType.OnLocked, 90], // policy overrides vault timeout when it's "onLocked"
|
||||
[null, VaultTimeoutStringType.OnLocked, VaultTimeoutStringType.OnLocked], // no policy, persist "onLocked" vault timeout
|
||||
[90, VaultTimeoutStringType.OnSleep, 90], // policy overrides vault timeout when it's "onSleep"
|
||||
[null, VaultTimeoutStringType.OnSleep, VaultTimeoutStringType.OnSleep], // no policy, persist "onSleep" vault timeout
|
||||
[90, VaultTimeoutStringType.OnIdle, 90], // policy overrides vault timeout when it's "onIdle"
|
||||
[null, VaultTimeoutStringType.OnIdle, VaultTimeoutStringType.OnIdle], // no policy, persist "onIdle" vault timeout
|
||||
])(
|
||||
"when policy is %s, and vault timeout is %s, returns %s",
|
||||
async (policy, vaultTimeout, expected) => {
|
||||
describe("no policy", () => {
|
||||
it("when vault timeout is null, returns default", async () => {
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of(policy === null ? [] : ([{ data: { minutes: policy } }] as unknown as Policy[])),
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, null, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(defaultVaultTimeout);
|
||||
});
|
||||
|
||||
it.each([
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
])("when vault timeout is %s, returns unchanged", async (vaultTimeout) => {
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(expected);
|
||||
},
|
||||
);
|
||||
expect(result).toBe(vaultTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: custom", () => {
|
||||
const policyMinutes = 30;
|
||||
|
||||
it.each([
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
])(
|
||||
"when vault timeout is %s and exceeds policy max, returns policy minutes",
|
||||
async (vaultTimeout) => {
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "custom", minutes: policyMinutes } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(policyMinutes);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([VaultTimeoutNumberType.OnMinute, policyMinutes])(
|
||||
"when vault timeout is %s and within policy max, returns unchanged",
|
||||
async (vaultTimeout) => {
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "custom", minutes: policyMinutes } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(vaultTimeout);
|
||||
},
|
||||
);
|
||||
|
||||
it("when vault timeout is Immediately, returns Immediately", async () => {
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "custom", minutes: policyMinutes } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(
|
||||
VAULT_TIMEOUT,
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(VaultTimeoutNumberType.Immediately);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: immediately", () => {
|
||||
it.each([
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
])(
|
||||
"when current timeout is %s, returns immediately or promoted value",
|
||||
async (currentTimeout) => {
|
||||
const expectedTimeout = VaultTimeoutNumberType.Immediately;
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(expectedTimeout);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "immediately" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, currentTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
);
|
||||
expect(result).toBe(expectedTimeout);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("policy type: onSystemLock", () => {
|
||||
it.each([
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
])(
|
||||
"when current timeout is %s, returns onLocked or promoted value",
|
||||
async (currentTimeout) => {
|
||||
const expectedTimeout = VaultTimeoutStringType.OnLocked;
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(expectedTimeout);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "onSystemLock" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, currentTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
);
|
||||
expect(result).toBe(expectedTimeout);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
])("when current timeout is numeric %s, returns unchanged", async (currentTimeout) => {
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "onSystemLock" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, currentTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(result).toBe(currentTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: onAppRestart", () => {
|
||||
it.each([
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
])("when current timeout is %s, returns onRestart", async (currentTimeout) => {
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "onAppRestart" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, currentTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(result).toBe(VaultTimeoutStringType.OnRestart);
|
||||
});
|
||||
|
||||
it.each([
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
])("when current timeout is %s, returns unchanged", async (currentTimeout) => {
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "onAppRestart" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, currentTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(result).toBe(currentTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy type: never", () => {
|
||||
it("when current timeout is never, returns never or promoted value", async () => {
|
||||
const expectedTimeout = VaultTimeoutStringType.Never;
|
||||
sessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(expectedTimeout);
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "never" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).toHaveBeenCalledWith(
|
||||
VaultTimeoutStringType.Never,
|
||||
);
|
||||
expect(result).toBe(expectedTimeout);
|
||||
});
|
||||
|
||||
it.each([
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutNumberType.EightHours,
|
||||
])("when current timeout is %s, returns unchanged", async (currentTimeout) => {
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ data: { type: "never" } }] as unknown as Policy[]),
|
||||
);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, currentTimeout, mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
|
||||
);
|
||||
|
||||
expect(sessionTimeoutTypeService.getOrPromoteToAvailable).not.toHaveBeenCalled();
|
||||
expect(result).toBe(currentTimeout);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setVaultTimeoutOptions", () => {
|
||||
@ -405,6 +648,7 @@ describe("VaultTimeoutSettingsService", () => {
|
||||
stateProvider,
|
||||
logService,
|
||||
defaultVaultTimeout,
|
||||
sessionTimeoutTypeService,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
EMPTY,
|
||||
Observable,
|
||||
catchError,
|
||||
combineLatest,
|
||||
defer,
|
||||
distinctUntilChanged,
|
||||
EMPTY,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap,
|
||||
@ -23,7 +24,6 @@ import { BiometricStateService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { getFirstPolicy } from "../../../admin-console/services/policy/default-policy.service";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { TokenService } from "../../../auth/abstractions/token.service";
|
||||
@ -31,9 +31,15 @@ import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PinStateServiceAbstraction } from "../../pin/pin-state.service.abstraction";
|
||||
import { MaximumSessionTimeoutPolicyData, SessionTimeoutTypeService } from "../../session-timeout";
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../abstractions/vault-timeout-settings.service";
|
||||
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type";
|
||||
import {
|
||||
isVaultTimeoutTypeNumeric,
|
||||
VaultTimeout,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "../types/vault-timeout.type";
|
||||
|
||||
import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION } from "./vault-timeout-settings.state";
|
||||
|
||||
@ -49,6 +55,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
private stateProvider: StateProvider,
|
||||
private logService: LogService,
|
||||
private defaultVaultTimeout: VaultTimeout,
|
||||
private sessionTimeoutTypeService: SessionTimeoutTypeService,
|
||||
) {}
|
||||
|
||||
async setVaultTimeoutOptions(
|
||||
@ -131,11 +138,25 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
|
||||
return combineLatest([
|
||||
this.stateProvider.getUserState$(VAULT_TIMEOUT, userId),
|
||||
this.getMaxVaultTimeoutPolicyByUserId$(userId),
|
||||
this.getMaxSessionTimeoutPolicyDataByUserId$(userId),
|
||||
]).pipe(
|
||||
switchMap(([currentVaultTimeout, maxVaultTimeoutPolicy]) => {
|
||||
return from(this.determineVaultTimeout(currentVaultTimeout, maxVaultTimeoutPolicy)).pipe(
|
||||
switchMap(([currentVaultTimeout, maxSessionTimeoutPolicyData]) => {
|
||||
this.logService.debug(
|
||||
"[VaultTimeoutSettingsService] Current vault timeout is %o for user id %s, max session policy %o",
|
||||
currentVaultTimeout,
|
||||
userId,
|
||||
maxSessionTimeoutPolicyData,
|
||||
);
|
||||
return from(
|
||||
this.determineVaultTimeout(currentVaultTimeout, maxSessionTimeoutPolicyData),
|
||||
).pipe(
|
||||
tap((vaultTimeout: VaultTimeout) => {
|
||||
this.logService.debug(
|
||||
"[VaultTimeoutSettingsService] Determined vault timeout is %o for user id %s",
|
||||
vaultTimeout,
|
||||
userId,
|
||||
);
|
||||
|
||||
// As a side effect, set the new value determined by determineVaultTimeout into state if it's different from the current
|
||||
if (vaultTimeout !== currentVaultTimeout) {
|
||||
return this.stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, userId);
|
||||
@ -155,28 +176,63 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
|
||||
private async determineVaultTimeout(
|
||||
currentVaultTimeout: VaultTimeout | null,
|
||||
maxVaultTimeoutPolicy: Policy | null,
|
||||
maxSessionTimeoutPolicyData: MaximumSessionTimeoutPolicyData | null,
|
||||
): Promise<VaultTimeout | null> {
|
||||
// if current vault timeout is null, apply the client specific default
|
||||
currentVaultTimeout = currentVaultTimeout ?? this.defaultVaultTimeout;
|
||||
|
||||
// If no policy applies, return the current vault timeout
|
||||
if (!maxVaultTimeoutPolicy) {
|
||||
if (maxSessionTimeoutPolicyData == null) {
|
||||
return currentVaultTimeout;
|
||||
}
|
||||
|
||||
// User is subject to a max vault timeout policy
|
||||
const maxVaultTimeoutPolicyData = maxVaultTimeoutPolicy.data;
|
||||
|
||||
// If the current vault timeout is not numeric, change it to the policy compliant value
|
||||
if (typeof currentVaultTimeout === "string") {
|
||||
return maxVaultTimeoutPolicyData.minutes;
|
||||
switch (maxSessionTimeoutPolicyData.type) {
|
||||
case "immediately":
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
);
|
||||
case "custom":
|
||||
case null:
|
||||
case undefined:
|
||||
if (currentVaultTimeout === VaultTimeoutNumberType.Immediately) {
|
||||
return currentVaultTimeout;
|
||||
}
|
||||
if (isVaultTimeoutTypeNumeric(currentVaultTimeout)) {
|
||||
return Math.min(currentVaultTimeout as number, maxSessionTimeoutPolicyData.minutes);
|
||||
}
|
||||
return maxSessionTimeoutPolicyData.minutes;
|
||||
case "onSystemLock":
|
||||
if (
|
||||
currentVaultTimeout === VaultTimeoutStringType.Never ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnRestart ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnLocked ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnIdle ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnSleep
|
||||
) {
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "onAppRestart":
|
||||
if (
|
||||
currentVaultTimeout === VaultTimeoutStringType.Never ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnLocked ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnIdle ||
|
||||
currentVaultTimeout === VaultTimeoutStringType.OnSleep
|
||||
) {
|
||||
return VaultTimeoutStringType.OnRestart;
|
||||
}
|
||||
break;
|
||||
case "never":
|
||||
if (currentVaultTimeout === VaultTimeoutStringType.Never) {
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutStringType.Never,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// For numeric vault timeouts, ensure they are smaller than maximum allowed value according to policy
|
||||
const policyCompliantTimeout = Math.min(currentVaultTimeout, maxVaultTimeoutPolicyData.minutes);
|
||||
|
||||
return policyCompliantTimeout;
|
||||
return currentVaultTimeout;
|
||||
}
|
||||
|
||||
private async setVaultTimeoutAction(userId: UserId, action: VaultTimeoutAction): Promise<void> {
|
||||
@ -198,14 +254,14 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
|
||||
return combineLatest([
|
||||
this.stateProvider.getUserState$(VAULT_TIMEOUT_ACTION, userId),
|
||||
this.getMaxVaultTimeoutPolicyByUserId$(userId),
|
||||
this.getMaxSessionTimeoutPolicyDataByUserId$(userId),
|
||||
]).pipe(
|
||||
switchMap(([currentVaultTimeoutAction, maxVaultTimeoutPolicy]) => {
|
||||
switchMap(([currentVaultTimeoutAction, maxSessionTimeoutPolicyData]) => {
|
||||
return from(
|
||||
this.determineVaultTimeoutAction(
|
||||
userId,
|
||||
currentVaultTimeoutAction,
|
||||
maxVaultTimeoutPolicy,
|
||||
maxSessionTimeoutPolicyData,
|
||||
),
|
||||
).pipe(
|
||||
tap((vaultTimeoutAction: VaultTimeoutAction) => {
|
||||
@ -235,7 +291,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
private async determineVaultTimeoutAction(
|
||||
userId: string,
|
||||
currentVaultTimeoutAction: VaultTimeoutAction | null,
|
||||
maxVaultTimeoutPolicy: Policy | null,
|
||||
maxSessionTimeoutPolicyData: MaximumSessionTimeoutPolicyData | null,
|
||||
): Promise<VaultTimeoutAction> {
|
||||
const availableVaultTimeoutActions = await this.getAvailableVaultTimeoutActions(userId);
|
||||
if (availableVaultTimeoutActions.length === 1) {
|
||||
@ -243,11 +299,13 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
}
|
||||
|
||||
if (
|
||||
maxVaultTimeoutPolicy?.data?.action &&
|
||||
availableVaultTimeoutActions.includes(maxVaultTimeoutPolicy.data.action)
|
||||
maxSessionTimeoutPolicyData?.action &&
|
||||
availableVaultTimeoutActions.includes(
|
||||
maxSessionTimeoutPolicyData.action as VaultTimeoutAction,
|
||||
)
|
||||
) {
|
||||
// return policy defined vault timeout action
|
||||
return maxVaultTimeoutPolicy.data.action;
|
||||
// return policy defined session timeout action
|
||||
return maxSessionTimeoutPolicyData.action as VaultTimeoutAction;
|
||||
}
|
||||
|
||||
// No policy applies from here on
|
||||
@ -262,14 +320,17 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
return currentVaultTimeoutAction;
|
||||
}
|
||||
|
||||
private getMaxVaultTimeoutPolicyByUserId$(userId: UserId): Observable<Policy | null> {
|
||||
private getMaxSessionTimeoutPolicyDataByUserId$(
|
||||
userId: UserId,
|
||||
): Observable<MaximumSessionTimeoutPolicyData | null> {
|
||||
if (!userId) {
|
||||
throw new Error("User id required. Cannot get max vault timeout policy.");
|
||||
throw new Error("User id required. Cannot get max session timeout policy.");
|
||||
}
|
||||
|
||||
return this.policyService
|
||||
.policiesByType$(PolicyType.MaximumVaultTimeout, userId)
|
||||
.pipe(getFirstPolicy);
|
||||
return this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId).pipe(
|
||||
getFirstPolicy,
|
||||
map((policy) => (policy?.data ?? null) as MaximumSessionTimeoutPolicyData | null),
|
||||
);
|
||||
}
|
||||
|
||||
private async getAvailableVaultTimeoutActions(userId?: string): Promise<VaultTimeoutAction[]> {
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
|
||||
|
||||
export interface MaximumVaultTimeoutPolicyData {
|
||||
minutes: number;
|
||||
action?: VaultTimeoutAction;
|
||||
}
|
||||
@ -5,13 +5,25 @@ export const VaultTimeoutStringType = {
|
||||
OnLocked: "onLocked", // -2
|
||||
OnSleep: "onSleep", // -3
|
||||
OnIdle: "onIdle", // -4
|
||||
Custom: "custom", // -100
|
||||
} as const;
|
||||
|
||||
export const VaultTimeoutNumberType = {
|
||||
Immediately: 0,
|
||||
OnMinute: 1,
|
||||
EightHours: 480,
|
||||
} as const;
|
||||
|
||||
export type VaultTimeout =
|
||||
| number // 0 or positive numbers only
|
||||
| (typeof VaultTimeoutNumberType)[keyof typeof VaultTimeoutNumberType]
|
||||
| number // 0 or positive numbers (in minutes). See VaultTimeoutNumberType for common numeric presets
|
||||
| (typeof VaultTimeoutStringType)[keyof typeof VaultTimeoutStringType];
|
||||
|
||||
export interface VaultTimeoutOption {
|
||||
name: string;
|
||||
value: VaultTimeout;
|
||||
}
|
||||
|
||||
export function isVaultTimeoutTypeNumeric(timeout: VaultTimeout): boolean {
|
||||
return typeof timeout === "number";
|
||||
}
|
||||
|
||||
@ -12,3 +12,4 @@ export { ConfirmKeyConnectorDomainComponent } from "./key-connector/confirm-key-
|
||||
export { SessionTimeoutSettingsComponent } from "./session-timeout/components/session-timeout-settings.component";
|
||||
export { SessionTimeoutSettingsComponentService } from "./session-timeout/services/session-timeout-settings-component.service";
|
||||
export { SessionTimeoutInputComponent } from "./session-timeout/components/session-timeout-input.component";
|
||||
export { SessionTimeoutInputLegacyComponent } from "./session-timeout/components/session-timeout-input-legacy.component";
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
<div [formGroup]="form" class="tw-mb-4">
|
||||
<bit-form-field [disableMargin]="!showCustom">
|
||||
<bit-label>{{ "vaultTimeout1" | i18n }}</bit-label>
|
||||
<bit-select formControlName="vaultTimeout">
|
||||
<bit-option
|
||||
*ngFor="let o of filteredVaultTimeoutOptions"
|
||||
[value]="o.value"
|
||||
[label]="o.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" *ngIf="showCustom" formGroupName="custom">
|
||||
<bit-form-field class="tw-col-span-6" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
min="0"
|
||||
formControlName="hours"
|
||||
aria-labelledby="maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "hours" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6 tw-self-end" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
min="0"
|
||||
name="minutes"
|
||||
formControlName="minutes"
|
||||
aria-labelledby="maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "minutes" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-hint *ngIf="vaultTimeoutPolicy != null && !exceedsMaximumTimeout">
|
||||
{{ "vaultTimeoutPolicyInEffect1" | i18n: vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes }}
|
||||
</bit-hint>
|
||||
<small *ngIf="!exceedsMinimumTimeout" class="tw-text-danger">
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i> {{ "vaultCustomTimeoutMinimum" | i18n }}
|
||||
</small>
|
||||
<small class="tw-text-danger" *ngIf="exceedsMaximumTimeout" id="maximum-error">
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||
{{
|
||||
"vaultTimeoutPolicyMaximumError" | i18n: vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
@ -0,0 +1,296 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
} from "@angular/forms";
|
||||
import { filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FormFieldModule, SelectModule } from "@bitwarden/components";
|
||||
|
||||
type VaultTimeoutForm = FormGroup<{
|
||||
vaultTimeout: FormControl<VaultTimeout | null>;
|
||||
custom: FormGroup<{
|
||||
hours: FormControl<number | null>;
|
||||
minutes: FormControl<number | null>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
type VaultTimeoutFormValue = VaultTimeoutForm["value"];
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link SessionTimeoutInputComponent} instead.
|
||||
*
|
||||
* TODO Cleanup once feature flag enabled: https://bitwarden.atlassian.net/browse/PM-27297
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "bit-session-timeout-input-legacy",
|
||||
templateUrl: "session-timeout-input-legacy.component.html",
|
||||
imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, SelectModule],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
multi: true,
|
||||
useExisting: SessionTimeoutInputLegacyComponent,
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
multi: true,
|
||||
useExisting: SessionTimeoutInputLegacyComponent,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SessionTimeoutInputLegacyComponent
|
||||
implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
static CUSTOM_VALUE = -100;
|
||||
static MIN_CUSTOM_MINUTES = 0;
|
||||
form: VaultTimeoutForm = this.formBuilder.group({
|
||||
vaultTimeout: [null],
|
||||
custom: this.formBuilder.group({
|
||||
hours: [null],
|
||||
minutes: [null],
|
||||
}),
|
||||
});
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() vaultTimeoutOptions: VaultTimeoutOption[];
|
||||
|
||||
vaultTimeoutPolicy: Policy;
|
||||
vaultTimeoutPolicyHours: number;
|
||||
vaultTimeoutPolicyMinutes: number;
|
||||
|
||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||
|
||||
protected canLockVault$: Observable<boolean>;
|
||||
private onChange: (vaultTimeout: VaultTimeout) => void;
|
||||
private validatorChange: () => void;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private policyService: PolicyService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
get showCustom() {
|
||||
return this.form.get("vaultTimeout").value === SessionTimeoutInputLegacyComponent.CUSTOM_VALUE;
|
||||
}
|
||||
|
||||
get exceedsMinimumTimeout(): boolean {
|
||||
return (
|
||||
!this.showCustom ||
|
||||
this.customTimeInMinutes() > SessionTimeoutInputLegacyComponent.MIN_CUSTOM_MINUTES
|
||||
);
|
||||
}
|
||||
|
||||
get exceedsMaximumTimeout(): boolean {
|
||||
return (
|
||||
this.showCustom &&
|
||||
this.customTimeInMinutes() >
|
||||
this.vaultTimeoutPolicyMinutes + 60 * this.vaultTimeoutPolicyHours
|
||||
);
|
||||
}
|
||||
|
||||
get filteredVaultTimeoutOptions(): VaultTimeoutOption[] {
|
||||
// by policy max value
|
||||
if (this.vaultTimeoutPolicy == null || this.vaultTimeoutPolicy.data == null) {
|
||||
return this.vaultTimeoutOptions;
|
||||
}
|
||||
|
||||
return this.vaultTimeoutOptions.filter((option) => {
|
||||
if (typeof option.value === "number") {
|
||||
return option.value <= this.vaultTimeoutPolicy.data.minutes;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
|
||||
),
|
||||
getFirstPolicy,
|
||||
filter((policy) => policy != null),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((policy) => {
|
||||
this.vaultTimeoutPolicy = policy;
|
||||
this.applyVaultTimeoutPolicy();
|
||||
});
|
||||
this.form.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value: VaultTimeoutFormValue) => {
|
||||
if (this.onChange) {
|
||||
this.onChange(this.getVaultTimeout(value));
|
||||
}
|
||||
});
|
||||
|
||||
// Assign the current value to the custom fields
|
||||
// so that if the user goes from a numeric value to custom
|
||||
// we can initialize the custom fields with the current value
|
||||
// ex: user picks 5 min, goes to custom, we want to show 0 hr, 5 min in the custom fields
|
||||
this.form.controls.vaultTimeout.valueChanges
|
||||
.pipe(
|
||||
filter((value) => value !== SessionTimeoutInputLegacyComponent.CUSTOM_VALUE),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((value) => {
|
||||
const current = typeof value === "string" ? 0 : Math.max(value, 0);
|
||||
|
||||
// This cannot emit an event b/c it would cause form.valueChanges to fire again
|
||||
// and we are already handling that above so just silently update
|
||||
// custom fields when vaultTimeout changes to a non-custom value
|
||||
this.form.patchValue(
|
||||
{
|
||||
custom: {
|
||||
hours: Math.floor(current / 60),
|
||||
minutes: current % 60,
|
||||
},
|
||||
},
|
||||
{ emitEvent: false },
|
||||
);
|
||||
});
|
||||
|
||||
this.canLockVault$ = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (
|
||||
!this.vaultTimeoutOptions.find(
|
||||
(p) => p.value === SessionTimeoutInputLegacyComponent.CUSTOM_VALUE,
|
||||
)
|
||||
) {
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("custom"),
|
||||
value: SessionTimeoutInputLegacyComponent.CUSTOM_VALUE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getVaultTimeout(value: VaultTimeoutFormValue) {
|
||||
if (value.vaultTimeout !== SessionTimeoutInputLegacyComponent.CUSTOM_VALUE) {
|
||||
return value.vaultTimeout;
|
||||
}
|
||||
|
||||
return value.custom.hours * 60 + value.custom.minutes;
|
||||
}
|
||||
|
||||
writeValue(value: number): void {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.vaultTimeoutOptions.every((p) => p.value !== value)) {
|
||||
this.form.setValue({
|
||||
vaultTimeout: SessionTimeoutInputLegacyComponent.CUSTOM_VALUE,
|
||||
custom: {
|
||||
hours: Math.floor(value / 60),
|
||||
minutes: value % 60,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.patchValue({
|
||||
vaultTimeout: value,
|
||||
});
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any): void {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(onTouched: any): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
validate(control: AbstractControl): ValidationErrors {
|
||||
if (this.vaultTimeoutPolicy && this.vaultTimeoutPolicy?.data?.minutes < control.value) {
|
||||
return { policyError: true };
|
||||
}
|
||||
|
||||
if (!this.exceedsMinimumTimeout) {
|
||||
return { minTimeoutError: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
registerOnValidatorChange(fn: () => void): void {
|
||||
this.validatorChange = fn;
|
||||
}
|
||||
|
||||
private customTimeInMinutes() {
|
||||
return this.form.value.custom.hours * 60 + this.form.value.custom.minutes;
|
||||
}
|
||||
|
||||
private applyVaultTimeoutPolicy() {
|
||||
this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60);
|
||||
this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60;
|
||||
|
||||
this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter((vaultTimeoutOption) => {
|
||||
// Always include the custom option
|
||||
if (vaultTimeoutOption.value === SessionTimeoutInputLegacyComponent.CUSTOM_VALUE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof vaultTimeoutOption.value === "number") {
|
||||
// Include numeric values that are less than or equal to the policy minutes
|
||||
return vaultTimeoutOption.value <= this.vaultTimeoutPolicy.data.minutes;
|
||||
}
|
||||
|
||||
// Exclude all string cases when there's a numeric policy defined
|
||||
return false;
|
||||
});
|
||||
|
||||
// Only call validator change if it's been set
|
||||
if (this.validatorChange) {
|
||||
this.validatorChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,47 +1,51 @@
|
||||
<div [formGroup]="form" class="tw-mb-4">
|
||||
<bit-form-field [disableMargin]="!showCustom">
|
||||
<bit-form-field [disableMargin]="!isCustomTimeoutType">
|
||||
<bit-label>{{ "vaultTimeout1" | i18n }}</bit-label>
|
||||
<bit-select formControlName="vaultTimeout">
|
||||
<bit-option
|
||||
*ngFor="let o of filteredVaultTimeoutOptions"
|
||||
[value]="o.value"
|
||||
[label]="o.name"
|
||||
></bit-option>
|
||||
@for (option of availableTimeoutOptions(); track option.value) {
|
||||
<bit-option [value]="option.value" [label]="option.name | i18n"></bit-option>
|
||||
}
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" *ngIf="showCustom" formGroupName="custom">
|
||||
<bit-form-field class="tw-col-span-6" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
min="0"
|
||||
formControlName="hours"
|
||||
aria-labelledby="maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "hours" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6 tw-self-end" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
min="0"
|
||||
name="minutes"
|
||||
formControlName="minutes"
|
||||
aria-labelledby="maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "minutes" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-hint *ngIf="vaultTimeoutPolicy != null && !exceedsMaximumTimeout">
|
||||
{{ "vaultTimeoutPolicyInEffect1" | i18n: vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes }}
|
||||
</bit-hint>
|
||||
<small *ngIf="!exceedsMinimumTimeout" class="tw-text-danger">
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i> {{ "vaultCustomTimeoutMinimum" | i18n }}
|
||||
</small>
|
||||
<small class="tw-text-danger" *ngIf="exceedsMaximumTimeout" id="maximum-error">
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||
{{
|
||||
"vaultTimeoutPolicyMaximumError" | i18n: vaultTimeoutPolicyHours : vaultTimeoutPolicyMinutes
|
||||
}}
|
||||
</small>
|
||||
@if (isCustomTimeoutType) {
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" formGroupName="custom">
|
||||
<bit-form-field class="tw-col-span-6" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
min="0"
|
||||
formControlName="hours"
|
||||
aria-describedby="session-timeout-maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "hours" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6 tw-self-start" disableMargin>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
[min]="customMinutesMin"
|
||||
max="59"
|
||||
formControlName="minutes"
|
||||
aria-describedby="session-timeout-maximum-error"
|
||||
/>
|
||||
<bit-label>{{ "minutes" | i18n }}</bit-label>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
}
|
||||
@if (form.hasError("maxTimeoutError")) {
|
||||
<div class="tw-mt-1 tw-text-danger tw-text-xs" id="session-timeout-maximum-error">
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||
{{
|
||||
"sessionTimeoutSettingsPolicyMaximumError"
|
||||
| i18n: maxSessionTimeoutPolicyHours : maxSessionTimeoutPolicyMinutes
|
||||
}}
|
||||
</div>
|
||||
} @else if (maxSessionTimeoutPolicyData != null) {
|
||||
@let policyTimeoutMessage = policyTimeoutMessage$ | async;
|
||||
@if (policyTimeoutMessage != null) {
|
||||
<bit-hint class="tw-mb-1" id="session-timeout-maximum-error">
|
||||
{{ policyTimeoutMessage }}
|
||||
</bit-hint>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1,90 +1,819 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { ComponentFixture, fakeAsync, flush, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
MaximumSessionTimeoutPolicyData,
|
||||
SessionTimeoutTypeService,
|
||||
} from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { SessionTimeoutSettingsComponentService } from "../services/session-timeout-settings-component.service";
|
||||
|
||||
import { SessionTimeoutInputComponent } from "./session-timeout-input.component";
|
||||
|
||||
describe("SessionTimeoutInputComponent", () => {
|
||||
let component: SessionTimeoutInputComponent;
|
||||
let fixture: ComponentFixture<SessionTimeoutInputComponent>;
|
||||
const policiesByType$ = jest.fn().mockReturnValue(new BehaviorSubject({}));
|
||||
const availableVaultTimeoutActions$ = jest.fn().mockReturnValue(new BehaviorSubject([]));
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
// Test constants
|
||||
const MOCK_USER_ID = "user-id" as UserId;
|
||||
const ONE_MINUTE = 1;
|
||||
const FIVE_MINUTES = 5;
|
||||
const FIFTEEN_MINUTES = 15;
|
||||
const THIRTY_MINUTES = 30;
|
||||
const ONE_HOUR = 60;
|
||||
const FOUR_HOURS = 240;
|
||||
const NINETY_MINUTES = 90;
|
||||
|
||||
// Mock services
|
||||
let mockPolicyService: MockProxy<PolicyService>;
|
||||
let mockSessionTimeoutSettingsComponentService: MockProxy<SessionTimeoutSettingsComponentService>;
|
||||
let mockSessionTimeoutTypeService: MockProxy<SessionTimeoutTypeService>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let accountService: AccountService;
|
||||
|
||||
// BehaviorSubjects for reactive testing
|
||||
let policies$: BehaviorSubject<Policy[]>;
|
||||
let availableTimeoutOptions: VaultTimeoutOption[];
|
||||
|
||||
beforeEach(async () => {
|
||||
// Initialize BehaviorSubjects
|
||||
policies$ = new BehaviorSubject<Policy[]>([]);
|
||||
|
||||
// Initialize available timeout options
|
||||
availableTimeoutOptions = [
|
||||
{ name: "oneMinute-used-i18n", value: ONE_MINUTE },
|
||||
{ name: "fiveMinutes-used-i18n", value: FIVE_MINUTES },
|
||||
{ name: "fifteenMinutes-used-i18n", value: FIFTEEN_MINUTES },
|
||||
{ name: "thirtyMinutes-used-i18n", value: THIRTY_MINUTES },
|
||||
{ name: "oneHour-used-i18n", value: ONE_HOUR },
|
||||
{ name: "fourHours-used-i18n", value: FOUR_HOURS },
|
||||
{ name: "onRestart-used-i18n", value: VaultTimeoutStringType.OnRestart },
|
||||
{ name: "onLocked-used-i18n", value: VaultTimeoutStringType.OnLocked },
|
||||
{ name: "never-used-i18n", value: VaultTimeoutStringType.Never },
|
||||
];
|
||||
|
||||
// Initialize mocks
|
||||
mockPolicyService = mock<PolicyService>();
|
||||
mockPolicyService.policiesByType$.mockReturnValue(policies$.asObservable());
|
||||
|
||||
accountService = mockAccountServiceWith(MOCK_USER_ID);
|
||||
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockI18nService.t.mockImplementation((key, ...args) => {
|
||||
if (args.length > 0) {
|
||||
return `${key}-used-i18n-${args.join("-")}`;
|
||||
}
|
||||
return `${key}-used-i18n`;
|
||||
});
|
||||
|
||||
mockLogService = mock<LogService>();
|
||||
|
||||
mockSessionTimeoutSettingsComponentService = mock<SessionTimeoutSettingsComponentService>();
|
||||
mockSessionTimeoutSettingsComponentService.policyFilteredTimeoutOptions$.mockReturnValue(
|
||||
of(availableTimeoutOptions),
|
||||
);
|
||||
|
||||
mockSessionTimeoutTypeService = mock<SessionTimeoutTypeService>();
|
||||
mockSessionTimeoutTypeService.isAvailable.mockResolvedValue(true);
|
||||
mockSessionTimeoutTypeService.getOrPromoteToAvailable.mockImplementation(
|
||||
async (timeout: VaultTimeout) => timeout,
|
||||
);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SessionTimeoutInputComponent],
|
||||
providers: [
|
||||
{ provide: PolicyService, useValue: { policiesByType$ } },
|
||||
{ provide: PolicyService, useValue: mockPolicyService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: { availableVaultTimeoutActions$ } },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{
|
||||
provide: SessionTimeoutSettingsComponentService,
|
||||
useValue: mockSessionTimeoutSettingsComponentService,
|
||||
},
|
||||
{ provide: SessionTimeoutTypeService, useValue: mockSessionTimeoutTypeService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SessionTimeoutInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.vaultTimeoutOptions = [
|
||||
{ name: "oneMinute", value: 1 },
|
||||
{ name: "fiveMinutes", value: 5 },
|
||||
{ name: "fifteenMinutes", value: 15 },
|
||||
{ name: "thirtyMinutes", value: 30 },
|
||||
{ name: "oneHour", value: 60 },
|
||||
{ name: "fourHours", value: 240 },
|
||||
{ name: "onRefresh", value: VaultTimeoutStringType.OnRestart },
|
||||
];
|
||||
fixture.detectChanges();
|
||||
fixture.componentRef.setInput("availableTimeoutOptions", availableTimeoutOptions);
|
||||
});
|
||||
|
||||
describe("form", () => {
|
||||
beforeEach(async () => {
|
||||
await component.ngOnInit();
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
describe("policy data subscription and initialization", () => {
|
||||
it("should initialize maxSessionTimeoutPolicyData to null when no policy exists", fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(component["maxSessionTimeoutPolicyData"]).toBeNull();
|
||||
}));
|
||||
|
||||
it("should set maxSessionTimeoutPolicyData when policy exists", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: NINETY_MINUTES,
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component["maxSessionTimeoutPolicyData"]).toEqual(policyData);
|
||||
}));
|
||||
|
||||
it("should trigger validatorChange callback when policy data changes", fakeAsync(() => {
|
||||
const validatorChangeFn = jest.fn();
|
||||
component.registerOnValidatorChange(validatorChangeFn);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: NINETY_MINUTES,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(validatorChangeFn).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it("should update form validation when policy data changes", fakeAsync(() => {
|
||||
const updateSpy = jest.spyOn(component.form.controls.custom, "updateValueAndValidity");
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: FIFTEEN_MINUTES,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(updateSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
it("invokes the onChange associated with `ControlValueAccessor`", () => {
|
||||
const onChange = jest.fn();
|
||||
component.registerOnChange(onChange);
|
||||
describe("policyTimeoutMessage$ observable", () => {
|
||||
it("should emit custom timeout message when policy has custom type", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: NINETY_MINUTES,
|
||||
};
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnRestart);
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(VaultTimeoutStringType.OnRestart);
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = null;
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBe(
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes-used-i18n-1-30",
|
||||
);
|
||||
}));
|
||||
|
||||
it("should emit immediately message when policy has immediately type", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "immediately",
|
||||
minutes: 0,
|
||||
};
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = null;
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBe(
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToImmediately-used-i18n",
|
||||
);
|
||||
}));
|
||||
|
||||
it("should emit onLocked message when policy has onSystemLock type", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "onSystemLock",
|
||||
minutes: 0,
|
||||
};
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = null;
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBe("sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked-used-i18n");
|
||||
}));
|
||||
|
||||
it("should emit onRestart message when policy has onAppRestart type", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "onAppRestart",
|
||||
minutes: 0,
|
||||
};
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = null;
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBe("sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart-used-i18n");
|
||||
}));
|
||||
|
||||
it("should emit null when policy has never type and promoted value is Never", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "never",
|
||||
minutes: 0,
|
||||
};
|
||||
|
||||
mockSessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(
|
||||
VaultTimeoutStringType.Never,
|
||||
);
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = "initial";
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBeNull();
|
||||
}));
|
||||
|
||||
it("should emit numeric timeout message when immediately is promoted to 1 minute", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "immediately",
|
||||
minutes: 0,
|
||||
};
|
||||
|
||||
mockSessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(ONE_MINUTE);
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = null;
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBe(
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes-used-i18n-0-1",
|
||||
);
|
||||
}));
|
||||
|
||||
it("should emit onRestart message when onSystemLock is promoted to OnRestart", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "onSystemLock",
|
||||
minutes: 0,
|
||||
};
|
||||
|
||||
mockSessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
);
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = null;
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBe("sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart-used-i18n");
|
||||
}));
|
||||
|
||||
it("should emit onRestart message when never is promoted to OnRestart", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "never",
|
||||
minutes: 0,
|
||||
};
|
||||
|
||||
mockSessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
);
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
let message: string | null = null;
|
||||
component["policyTimeoutMessage$"].subscribe((msg) => (message = msg));
|
||||
flush();
|
||||
|
||||
expect(message).toBe("sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart-used-i18n");
|
||||
}));
|
||||
});
|
||||
|
||||
it("updates custom value to match preset option", () => {
|
||||
// 1 hour
|
||||
component.form.controls.vaultTimeout.setValue(60);
|
||||
describe("form value changes subscription", () => {
|
||||
it("should call onChange with vault timeout when form is valid and in custom mode", fakeAsync(() => {
|
||||
const onChange = jest.fn();
|
||||
component.registerOnChange(onChange);
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 1, minutes: 0 });
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
// 17 minutes
|
||||
component.form.controls.vaultTimeout.setValue(17);
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 1, minutes: 30 });
|
||||
flush();
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 0, minutes: 17 });
|
||||
expect(onChange).toHaveBeenCalledWith(NINETY_MINUTES);
|
||||
}));
|
||||
|
||||
// 2.25 hours
|
||||
component.form.controls.vaultTimeout.setValue(135);
|
||||
it("should call onChange when form changes to non-custom mode", fakeAsync(() => {
|
||||
const onChange = jest.fn();
|
||||
component.registerOnChange(onChange);
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 2, minutes: 15 });
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
onChange.mockClear();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(FIFTEEN_MINUTES);
|
||||
flush();
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(FIFTEEN_MINUTES);
|
||||
}));
|
||||
|
||||
it("should not call onChange when custom controls are invalid", fakeAsync(() => {
|
||||
const onChange = jest.fn();
|
||||
component.registerOnChange(onChange);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
flush();
|
||||
|
||||
onChange.mockClear();
|
||||
|
||||
component.form.controls.custom.controls.hours.setValue(null);
|
||||
flush();
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it("should not call onChange when vaultTimeout is null", fakeAsync(() => {
|
||||
const onChange = jest.fn();
|
||||
component.registerOnChange(onChange);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
onChange.mockClear();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(null);
|
||||
flush();
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
it("sets custom timeout to 0 when a preset string option is selected", () => {
|
||||
// Set custom value to random values
|
||||
component.form.controls.custom.setValue({ hours: 1, minutes: 1 });
|
||||
describe("custom fields initialization from vaultTimeout changes", () => {
|
||||
it("should update custom fields when vaultTimeout changes to numeric value", fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnLocked);
|
||||
component.form.controls.vaultTimeout.setValue(NINETY_MINUTES);
|
||||
flush();
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 0, minutes: 0 });
|
||||
expect(component.form.value.custom).toEqual({ hours: 1, minutes: 30 });
|
||||
}));
|
||||
|
||||
it.each([
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
])(
|
||||
"should set custom fields to 8 hours when vaultTimeout changes to %s",
|
||||
fakeAsync((timeoutType: VaultTimeout) => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
component.form.controls.custom.setValue({ hours: 1, minutes: 30 });
|
||||
component.form.controls.vaultTimeout.setValue(timeoutType);
|
||||
flush();
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 8, minutes: 0 });
|
||||
}),
|
||||
);
|
||||
|
||||
it("should mark custom fields as touched after update", fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(ONE_HOUR);
|
||||
flush();
|
||||
|
||||
expect(component.form.controls.custom.controls.hours.touched).toBe(true);
|
||||
expect(component.form.controls.custom.controls.minutes.touched).toBe(true);
|
||||
}));
|
||||
|
||||
it("should not update custom fields when vaultTimeout changes to Custom", fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
component.form.controls.custom.setValue({ hours: 5, minutes: 15 });
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
flush();
|
||||
|
||||
expect(component.form.value.custom).toEqual({ hours: 5, minutes: 15 });
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCustomTimeoutType", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it("should return true when vaultTimeout is Custom", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
flush();
|
||||
|
||||
expect(component.isCustomTimeoutType).toBe(true);
|
||||
}));
|
||||
|
||||
it.each([
|
||||
ONE_MINUTE,
|
||||
FIFTEEN_MINUTES,
|
||||
ONE_HOUR,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.Never,
|
||||
])(
|
||||
"should return false when vaultTimeout is %s",
|
||||
fakeAsync((timeout: VaultTimeout) => {
|
||||
component.form.controls.vaultTimeout.setValue(timeout);
|
||||
flush();
|
||||
|
||||
expect(component.isCustomTimeoutType).toBe(false);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("customMinutesMin", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it("should return 1 when hours is 0", fakeAsync(() => {
|
||||
component.form.controls.custom.controls.hours.setValue(0);
|
||||
flush();
|
||||
|
||||
expect(component.customMinutesMin).toBe(1);
|
||||
}));
|
||||
|
||||
it.each([1, 2, 5, 10])(
|
||||
"should return 0 when hours is %s",
|
||||
fakeAsync((hours: number) => {
|
||||
component.form.controls.custom.controls.hours.setValue(hours);
|
||||
flush();
|
||||
|
||||
expect(component.customMinutesMin).toBe(0);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("maxSessionTimeoutPolicyHours", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it("should return 0 when no policy exists", fakeAsync(() => {
|
||||
expect(component.maxSessionTimeoutPolicyHours).toBe(0);
|
||||
}));
|
||||
|
||||
it.each([
|
||||
{ minutes: ONE_HOUR, expectedHours: 1 },
|
||||
{ minutes: NINETY_MINUTES, expectedHours: 1 },
|
||||
{ minutes: FOUR_HOURS, expectedHours: 4 },
|
||||
{ minutes: 300, expectedHours: 5 },
|
||||
])(
|
||||
"should return $expectedHours when policy minutes is $minutes",
|
||||
fakeAsync(({ minutes, expectedHours }: { minutes: number; expectedHours: number }) => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component.maxSessionTimeoutPolicyHours).toBe(expectedHours);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("maxSessionTimeoutPolicyMinutes", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it("should return 0 when no policy exists", fakeAsync(() => {
|
||||
expect(component.maxSessionTimeoutPolicyMinutes).toBe(0);
|
||||
}));
|
||||
|
||||
it.each([
|
||||
{ minutes: ONE_HOUR, expectedMinutes: 0 },
|
||||
{ minutes: NINETY_MINUTES, expectedMinutes: 30 },
|
||||
{ minutes: 65, expectedMinutes: 5 },
|
||||
{ minutes: 137, expectedMinutes: 17 },
|
||||
])(
|
||||
"should return $expectedMinutes when policy minutes is $minutes",
|
||||
fakeAsync(({ minutes, expectedMinutes }: { minutes: number; expectedMinutes: number }) => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component.maxSessionTimeoutPolicyMinutes).toBe(expectedMinutes);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("exceedsPolicyMaximumTimeout", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it("should return true when custom timeout exceeds policy maximum", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: ONE_HOUR,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 2, minutes: 0 });
|
||||
flush();
|
||||
|
||||
expect(component.exceedsPolicyMaximumTimeout).toBe(true);
|
||||
}));
|
||||
|
||||
it("should return false when no policy exists", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 100, minutes: 0 });
|
||||
flush();
|
||||
|
||||
expect(component.exceedsPolicyMaximumTimeout).toBe(false);
|
||||
}));
|
||||
|
||||
it("should return false when policy type is not custom", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "immediately",
|
||||
minutes: 0,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 10, minutes: 0 });
|
||||
flush();
|
||||
|
||||
expect(component.exceedsPolicyMaximumTimeout).toBe(false);
|
||||
}));
|
||||
|
||||
it("should return false when policy type is custom and form timeout is not custom", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: FIFTEEN_MINUTES,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(ONE_HOUR);
|
||||
flush();
|
||||
|
||||
expect(component.exceedsPolicyMaximumTimeout).toBe(false);
|
||||
}));
|
||||
|
||||
it("should return false when custom timeout equals policy maximum", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: ONE_HOUR,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 1, minutes: 0 });
|
||||
flush();
|
||||
|
||||
expect(component.exceedsPolicyMaximumTimeout).toBe(false);
|
||||
}));
|
||||
|
||||
it("should return false when custom timeout is below policy maximum", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: FOUR_HOURS,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 2, minutes: 30 });
|
||||
flush();
|
||||
|
||||
expect(component.exceedsPolicyMaximumTimeout).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("writeValue", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it("should do nothing when value is null", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(ONE_HOUR);
|
||||
flush();
|
||||
|
||||
component.writeValue(null);
|
||||
flush();
|
||||
|
||||
expect(component.form.controls.vaultTimeout.value).toBe(ONE_HOUR);
|
||||
}));
|
||||
|
||||
it("should set form to custom mode when value doesn't match any available option", fakeAsync(() => {
|
||||
component.writeValue(NINETY_MINUTES);
|
||||
flush();
|
||||
|
||||
expect(component.form.controls.vaultTimeout.value).toBe(VaultTimeoutStringType.Custom);
|
||||
expect(component.form.controls.custom.value).toEqual({ hours: 1, minutes: 30 });
|
||||
}));
|
||||
|
||||
it.each([ONE_MINUTE, FIVE_MINUTES, FIFTEEN_MINUTES, THIRTY_MINUTES, ONE_HOUR, FOUR_HOURS])(
|
||||
"should set vaultTimeout directly when numeric value %s matches preset option",
|
||||
fakeAsync((timeout: number) => {
|
||||
component.writeValue(timeout);
|
||||
flush();
|
||||
|
||||
expect(component.form.controls.vaultTimeout.value).toBe(timeout);
|
||||
}),
|
||||
);
|
||||
|
||||
it.each([
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.Never,
|
||||
])(
|
||||
"should set vaultTimeout directly when string value %s matches preset option",
|
||||
fakeAsync((timeout: VaultTimeout) => {
|
||||
component.writeValue(timeout);
|
||||
flush();
|
||||
|
||||
expect(component.form.controls.vaultTimeout.value).toBe(timeout);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("validate", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
}));
|
||||
|
||||
it("should return null when vaultTimeout is not custom", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(ONE_HOUR);
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toBeNull();
|
||||
}));
|
||||
|
||||
it("should return required error when vaultTimeout is custom and hours is null", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.controls.hours.setValue(null);
|
||||
component.form.controls.custom.controls.minutes.setValue(30);
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toEqual({ required: true });
|
||||
}));
|
||||
|
||||
it("should return required error when vaultTimeout is custom and minutes is null", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.controls.hours.setValue(1);
|
||||
component.form.controls.custom.controls.minutes.setValue(null);
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toEqual({ required: true });
|
||||
}));
|
||||
|
||||
it("should return required error when vaultTimeout is custom and both hours and minutes are null", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.controls.hours.setValue(null);
|
||||
component.form.controls.custom.controls.minutes.setValue(null);
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toEqual({ required: true });
|
||||
}));
|
||||
|
||||
it("should return minTimeoutError when total minutes is 0", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 0, minutes: 0 });
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toEqual({ minTimeoutError: true });
|
||||
}));
|
||||
|
||||
it("should return maxTimeoutError when exceeds policy maximum", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: ONE_HOUR,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 2, minutes: 0 });
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toEqual({ maxTimeoutError: true });
|
||||
}));
|
||||
|
||||
it("should return null when custom values are valid and within policy limit", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "custom",
|
||||
minutes: FOUR_HOURS,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 2, minutes: 30 });
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toBeNull();
|
||||
}));
|
||||
|
||||
it("should return null when custom values are valid and no policy exists", fakeAsync(() => {
|
||||
component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.Custom);
|
||||
component.form.controls.custom.setValue({ hours: 5, minutes: 15 });
|
||||
flush();
|
||||
|
||||
const result = component.validate(component.form);
|
||||
|
||||
expect(result).toBeNull();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit } from "@angular/core";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
input,
|
||||
OnInit,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
AbstractControl,
|
||||
AbstractControlOptions,
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
@ -13,26 +20,32 @@ import {
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs";
|
||||
import { filter, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import {
|
||||
MaximumSessionTimeoutPolicyData,
|
||||
SessionTimeoutTypeService,
|
||||
} from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
isVaultTimeoutTypeNumeric,
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FormFieldModule, SelectModule } from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
type VaultTimeoutForm = FormGroup<{
|
||||
type SessionTimeoutForm = FormGroup<{
|
||||
vaultTimeout: FormControl<VaultTimeout | null>;
|
||||
custom: FormGroup<{
|
||||
hours: FormControl<number | null>;
|
||||
@ -40,10 +53,8 @@ type VaultTimeoutForm = FormGroup<{
|
||||
}>;
|
||||
}>;
|
||||
|
||||
type VaultTimeoutFormValue = VaultTimeoutForm["value"];
|
||||
type SessionTimeoutFormValue = SessionTimeoutForm["value"];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "bit-session-timeout-input",
|
||||
templateUrl: "session-timeout-input.component.html",
|
||||
@ -60,111 +71,110 @@ type VaultTimeoutFormValue = VaultTimeoutForm["value"];
|
||||
useExisting: SessionTimeoutInputComponent,
|
||||
},
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SessionTimeoutInputComponent
|
||||
implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
static CUSTOM_VALUE = -100;
|
||||
static MIN_CUSTOM_MINUTES = 0;
|
||||
form: VaultTimeoutForm = this.formBuilder.group({
|
||||
vaultTimeout: [null],
|
||||
custom: this.formBuilder.group({
|
||||
hours: [null],
|
||||
minutes: [null],
|
||||
}),
|
||||
});
|
||||
export class SessionTimeoutInputComponent implements ControlValueAccessor, Validator, OnInit {
|
||||
static readonly MIN_CUSTOM_MINUTES = 0;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() vaultTimeoutOptions: VaultTimeoutOption[];
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
private readonly policyService = inject(PolicyService);
|
||||
private readonly i18nService = inject(I18nService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly sessionTimeoutTypeService = inject(SessionTimeoutTypeService);
|
||||
private readonly logService = inject(LogService);
|
||||
|
||||
vaultTimeoutPolicy: Policy;
|
||||
vaultTimeoutPolicyHours: number;
|
||||
vaultTimeoutPolicyMinutes: number;
|
||||
readonly availableTimeoutOptions = input.required<VaultTimeoutOption[]>();
|
||||
|
||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||
protected maxSessionTimeoutPolicyData: MaximumSessionTimeoutPolicyData | null = null;
|
||||
protected policyTimeoutMessage$!: Observable<string | null>;
|
||||
|
||||
protected canLockVault$: Observable<boolean>;
|
||||
private onChange: (vaultTimeout: VaultTimeout) => void;
|
||||
private validatorChange: () => void;
|
||||
private destroy$ = new Subject<void>();
|
||||
readonly form: SessionTimeoutForm = this.formBuilder.group(
|
||||
{
|
||||
vaultTimeout: [null as VaultTimeout | null],
|
||||
custom: this.formBuilder.group({
|
||||
hours: [0, [Validators.required, Validators.min(0)]],
|
||||
minutes: [0, [Validators.required, Validators.min(0), Validators.max(59)]],
|
||||
}),
|
||||
},
|
||||
{ validators: [this.formValidator.bind(this)] } as AbstractControlOptions,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private policyService: PolicyService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
private onChange: ((vaultTimeout: VaultTimeout) => void) | null = null;
|
||||
private validatorChange: (() => void) | null = null;
|
||||
|
||||
get showCustom() {
|
||||
return this.form.get("vaultTimeout").value === SessionTimeoutInputComponent.CUSTOM_VALUE;
|
||||
get isCustomTimeoutType(): boolean {
|
||||
return this.form.controls.vaultTimeout.value === VaultTimeoutStringType.Custom;
|
||||
}
|
||||
|
||||
get exceedsMinimumTimeout(): boolean {
|
||||
get customMinutesMin(): number {
|
||||
return this.form.controls.custom.controls.hours.value === 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
get exceedsPolicyMaximumTimeout(): boolean {
|
||||
return (
|
||||
!this.showCustom ||
|
||||
this.customTimeInMinutes() > SessionTimeoutInputComponent.MIN_CUSTOM_MINUTES
|
||||
this.maxSessionTimeoutPolicyData?.type === VaultTimeoutStringType.Custom &&
|
||||
this.isCustomTimeoutType &&
|
||||
this.getTotalMinutesFromCustomValue(this.form.value.custom) >
|
||||
this.maxSessionTimeoutPolicyMinutes + 60 * this.maxSessionTimeoutPolicyHours
|
||||
);
|
||||
}
|
||||
|
||||
get exceedsMaximumTimeout(): boolean {
|
||||
return (
|
||||
this.showCustom &&
|
||||
this.customTimeInMinutes() >
|
||||
this.vaultTimeoutPolicyMinutes + 60 * this.vaultTimeoutPolicyHours
|
||||
ngOnInit(): void {
|
||||
const maximumSessionTimeoutPolicyData$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
|
||||
),
|
||||
getFirstPolicy,
|
||||
filter((policy) => policy != null),
|
||||
map((policy) => policy.data as MaximumSessionTimeoutPolicyData),
|
||||
);
|
||||
}
|
||||
|
||||
get filteredVaultTimeoutOptions(): VaultTimeoutOption[] {
|
||||
// by policy max value
|
||||
if (this.vaultTimeoutPolicy == null || this.vaultTimeoutPolicy.data == null) {
|
||||
return this.vaultTimeoutOptions;
|
||||
}
|
||||
this.policyTimeoutMessage$ = maximumSessionTimeoutPolicyData$.pipe(
|
||||
switchMap((policyData) => this.getPolicyTimeoutMessage(policyData)),
|
||||
);
|
||||
|
||||
return this.vaultTimeoutOptions.filter((option) => {
|
||||
if (typeof option.value === "number") {
|
||||
return option.value <= this.vaultTimeoutPolicy.data.minutes;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
|
||||
),
|
||||
getFirstPolicy,
|
||||
filter((policy) => policy != null),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((policy) => {
|
||||
this.vaultTimeoutPolicy = policy;
|
||||
this.applyVaultTimeoutPolicy();
|
||||
});
|
||||
this.form.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value: VaultTimeoutFormValue) => {
|
||||
if (this.onChange) {
|
||||
this.onChange(this.getVaultTimeout(value));
|
||||
maximumSessionTimeoutPolicyData$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((policyData) => {
|
||||
this.maxSessionTimeoutPolicyData = policyData;
|
||||
// Re-validate custom form group with new policy data
|
||||
this.form.controls.custom.updateValueAndValidity();
|
||||
// Trigger validator change when policy data changes
|
||||
if (this.validatorChange) {
|
||||
this.validatorChange();
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to form value changes
|
||||
this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => {
|
||||
if (this.onChange) {
|
||||
const vaultTimeout = this.getVaultTimeout(value);
|
||||
if (vaultTimeout != null) {
|
||||
// Only call onChange if the form is valid
|
||||
// For non-numeric values, we don't need to validate custom fields
|
||||
const isValid = !this.isCustomTimeoutType || this.form.controls.custom.valid;
|
||||
if (isValid) {
|
||||
this.onChange(vaultTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Assign the current value to the custom fields
|
||||
// so that if the user goes from a numeric value to custom
|
||||
// we can initialize the custom fields with the current value
|
||||
// ex: user picks 5 min, goes to custom, we want to show 0 hr, 5 min in the custom fields
|
||||
this.form.controls.vaultTimeout.valueChanges
|
||||
.pipe(
|
||||
filter((value) => value !== SessionTimeoutInputComponent.CUSTOM_VALUE),
|
||||
takeUntil(this.destroy$),
|
||||
filter((value) => value != null && value !== VaultTimeoutStringType.Custom),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((value) => {
|
||||
const current = typeof value === "string" ? 0 : Math.max(value, 0);
|
||||
const current = isVaultTimeoutTypeNumeric(value)
|
||||
? (value as number)
|
||||
: VaultTimeoutNumberType.EightHours;
|
||||
|
||||
// This cannot emit an event b/c it would cause form.valueChanges to fire again
|
||||
// and we are already handling that above so just silently update
|
||||
@ -178,112 +188,169 @@ export class SessionTimeoutInputComponent
|
||||
},
|
||||
{ emitEvent: false },
|
||||
);
|
||||
|
||||
this.form.controls.custom.markAllAsTouched();
|
||||
});
|
||||
|
||||
this.canLockVault$ = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
get maxSessionTimeoutPolicyHours(): number {
|
||||
return Math.floor((this.maxSessionTimeoutPolicyData?.minutes ?? 0) / 60);
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (
|
||||
!this.vaultTimeoutOptions.find((p) => p.value === SessionTimeoutInputComponent.CUSTOM_VALUE)
|
||||
) {
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("custom"),
|
||||
value: SessionTimeoutInputComponent.CUSTOM_VALUE,
|
||||
});
|
||||
}
|
||||
get maxSessionTimeoutPolicyMinutes(): number {
|
||||
return (this.maxSessionTimeoutPolicyData?.minutes ?? 0) % 60;
|
||||
}
|
||||
|
||||
getVaultTimeout(value: VaultTimeoutFormValue) {
|
||||
if (value.vaultTimeout !== SessionTimeoutInputComponent.CUSTOM_VALUE) {
|
||||
return value.vaultTimeout;
|
||||
}
|
||||
|
||||
return value.custom.hours * 60 + value.custom.minutes;
|
||||
}
|
||||
|
||||
writeValue(value: number): void {
|
||||
writeValue(value: VaultTimeout | null): void {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.vaultTimeoutOptions.every((p) => p.value !== value)) {
|
||||
// Normalize the custom numeric value to preset (i.e. 1 minute), otherwise set as custom
|
||||
const options = this.availableTimeoutOptions();
|
||||
const matchingOption = options.some((opt) => opt.value === value);
|
||||
if (!matchingOption) {
|
||||
this.logService.debug(
|
||||
`[SessionTimeoutInputComponent] form control write value as custom ${value}`,
|
||||
);
|
||||
this.form.setValue({
|
||||
vaultTimeout: SessionTimeoutInputComponent.CUSTOM_VALUE,
|
||||
vaultTimeout: VaultTimeoutStringType.Custom,
|
||||
custom: {
|
||||
hours: Math.floor(value / 60),
|
||||
minutes: value % 60,
|
||||
hours: Math.floor((value as number) / 60),
|
||||
minutes: (value as number) % 60,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.debug(
|
||||
`[SessionTimeoutInputComponent] form control write value as preset ${value}`,
|
||||
);
|
||||
|
||||
// For string values (e.g., "onLocked", "never"), set directly
|
||||
this.form.patchValue({
|
||||
vaultTimeout: value,
|
||||
});
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any): void {
|
||||
registerOnChange(onChange: (vaultTimeout: VaultTimeout) => void): void {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(onTouched: any): void {
|
||||
registerOnTouched(_onTouched: () => void): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
setDisabledState?(_isDisabled: boolean): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
validate(control: AbstractControl): ValidationErrors {
|
||||
if (this.vaultTimeoutPolicy && this.vaultTimeoutPolicy?.data?.minutes < control.value) {
|
||||
return { policyError: true };
|
||||
}
|
||||
|
||||
if (!this.exceedsMinimumTimeout) {
|
||||
return { minTimeoutError: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
validate(_: AbstractControl): ValidationErrors | null {
|
||||
return this.form.errors;
|
||||
}
|
||||
|
||||
registerOnValidatorChange(fn: () => void): void {
|
||||
this.validatorChange = fn;
|
||||
}
|
||||
|
||||
private customTimeInMinutes() {
|
||||
return this.form.value.custom.hours * 60 + this.form.value.custom.minutes;
|
||||
private getTotalMinutesFromCustomValue(customValue: SessionTimeoutFormValue["custom"]): number {
|
||||
const hours = customValue?.hours ?? 0;
|
||||
const minutes = customValue?.minutes ?? 0;
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
private applyVaultTimeoutPolicy() {
|
||||
this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60);
|
||||
this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60;
|
||||
private formValidator(control: AbstractControl): ValidationErrors | null {
|
||||
const formValue = control.value as SessionTimeoutFormValue;
|
||||
const isCustomMode = formValue.vaultTimeout === VaultTimeoutStringType.Custom;
|
||||
|
||||
this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter((vaultTimeoutOption) => {
|
||||
// Always include the custom option
|
||||
if (vaultTimeoutOption.value === SessionTimeoutInputComponent.CUSTOM_VALUE) {
|
||||
return true;
|
||||
// Only validate when in custom mode
|
||||
if (!isCustomMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hours = formValue.custom?.hours;
|
||||
const minutes = formValue.custom?.minutes;
|
||||
|
||||
if (hours == null || minutes == null) {
|
||||
return { required: true };
|
||||
}
|
||||
|
||||
const totalMinutes = this.getTotalMinutesFromCustomValue(formValue.custom);
|
||||
if (totalMinutes === 0) {
|
||||
return { minTimeoutError: true };
|
||||
}
|
||||
|
||||
if (this.exceedsPolicyMaximumTimeout) {
|
||||
return { maxTimeoutError: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getVaultTimeout(value: SessionTimeoutFormValue): VaultTimeout | null {
|
||||
if (value.vaultTimeout !== VaultTimeoutStringType.Custom) {
|
||||
return value.vaultTimeout ?? null;
|
||||
}
|
||||
|
||||
return this.getTotalMinutesFromCustomValue(value.custom);
|
||||
}
|
||||
|
||||
private async getPolicyTimeoutMessage(
|
||||
policyData: MaximumSessionTimeoutPolicyData,
|
||||
): Promise<string | null> {
|
||||
const timeout = await this.getPolicyAppliedTimeout(policyData);
|
||||
|
||||
switch (timeout) {
|
||||
case null:
|
||||
// Don't display the policy message
|
||||
return null;
|
||||
case VaultTimeoutNumberType.Immediately:
|
||||
return this.i18nService.t("sessionTimeoutSettingsPolicySetDefaultTimeoutToImmediately");
|
||||
case VaultTimeoutStringType.OnLocked:
|
||||
return this.i18nService.t("sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked");
|
||||
case VaultTimeoutStringType.OnRestart:
|
||||
return this.i18nService.t("sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart");
|
||||
default:
|
||||
if (isVaultTimeoutTypeNumeric(timeout)) {
|
||||
const hours = Math.floor((timeout as number) / 60);
|
||||
const minutes = (timeout as number) % 60;
|
||||
return this.i18nService.t(
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes",
|
||||
hours,
|
||||
minutes,
|
||||
);
|
||||
}
|
||||
throw new Error("Invalid timeout parameter");
|
||||
}
|
||||
}
|
||||
|
||||
private async getPolicyAppliedTimeout(
|
||||
policyData: MaximumSessionTimeoutPolicyData,
|
||||
): Promise<VaultTimeout | null> {
|
||||
switch (policyData.type) {
|
||||
case "immediately":
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
);
|
||||
case "onSystemLock":
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
);
|
||||
case "onAppRestart":
|
||||
return VaultTimeoutStringType.OnRestart;
|
||||
case "never": {
|
||||
const timeout = await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutStringType.Never,
|
||||
);
|
||||
if (timeout == VaultTimeoutStringType.Never) {
|
||||
// Don't display policy message, when the policy doesn't change the available timeout options
|
||||
return null;
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
if (typeof vaultTimeoutOption.value === "number") {
|
||||
// Include numeric values that are less than or equal to the policy minutes
|
||||
return vaultTimeoutOption.value <= this.vaultTimeoutPolicy.data.minutes;
|
||||
}
|
||||
|
||||
// Exclude all string cases when there's a numeric policy defined
|
||||
return false;
|
||||
});
|
||||
|
||||
// Only call validator change if it's been set
|
||||
if (this.validatorChange) {
|
||||
this.validatorChange();
|
||||
case "custom":
|
||||
default:
|
||||
return policyData.minutes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
<div [formGroup]="formGroup">
|
||||
<bit-session-timeout-input
|
||||
[vaultTimeoutOptions]="availableTimeoutOptions$ | async"
|
||||
[formControl]="formGroup.controls.timeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</bit-session-timeout-input>
|
||||
@if (availableTimeoutOptions$ | async; as options) {
|
||||
<bit-session-timeout-input
|
||||
[availableTimeoutOptions]="options"
|
||||
[formControl]="formGroup.controls.timeout"
|
||||
>
|
||||
</bit-session-timeout-input>
|
||||
}
|
||||
|
||||
<bit-form-field [disableMargin]="true">
|
||||
<bit-label>{{ "sessionTimeoutSettingsAction" | i18n }}</bit-label>
|
||||
@ -18,14 +19,10 @@
|
||||
}
|
||||
</bit-select>
|
||||
|
||||
@if (!canLock) {
|
||||
<bit-hint>{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}<br /></bit-hint>
|
||||
@if (!canLock && supportsLock) {
|
||||
<bit-hint>{{ "sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction" | i18n }}</bit-hint>
|
||||
} @else if ((sessionTimeoutActionFromPolicy$ | async) != null) {
|
||||
<bit-hint>{{ "sessionTimeoutSettingsManagedByOrganization" | i18n }}</bit-hint>
|
||||
}
|
||||
</bit-form-field>
|
||||
|
||||
@if (hasVaultTimeoutPolicy$ | async) {
|
||||
<bit-hint class="tw-mt-4">
|
||||
{{ "vaultTimeoutPolicyAffectingOptions" | i18n }}
|
||||
</bit-hint>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -4,11 +4,15 @@ import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, filter, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
MaximumVaultTimeoutPolicyData,
|
||||
MaximumSessionTimeoutPolicyData,
|
||||
SessionTimeoutTypeService,
|
||||
} from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutOption,
|
||||
@ -16,6 +20,7 @@ import {
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
@ -39,6 +44,8 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
let accountService: FakeAccountService;
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
const mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
const mockSessionTimeoutTypeService = mock<SessionTimeoutTypeService>();
|
||||
|
||||
const mockUserId = "user-id" as UserId;
|
||||
const mockEmail = "test@example.com";
|
||||
@ -46,6 +53,7 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
const mockInitialTimeoutAction = VaultTimeoutAction.Lock;
|
||||
let refreshTimeoutActionSettings$: BehaviorSubject<void>;
|
||||
let availableTimeoutOptions$: BehaviorSubject<VaultTimeoutOption[]>;
|
||||
let policies$: BehaviorSubject<Policy[]>;
|
||||
|
||||
beforeEach(async () => {
|
||||
refreshTimeoutActionSettings$ = new BehaviorSubject<void>(undefined);
|
||||
@ -58,6 +66,7 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
{ name: "onIdle-used-i18n", value: VaultTimeoutStringType.OnIdle },
|
||||
{ name: "never-used-i18n", value: VaultTimeoutStringType.Never },
|
||||
]);
|
||||
policies$ = new BehaviorSubject<Policy[]>([]);
|
||||
|
||||
mockVaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
mockSessionTimeoutSettingsComponentService = mock<SessionTimeoutSettingsComponentService>();
|
||||
@ -79,9 +88,10 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
mockVaultTimeoutSettingsService.availableVaultTimeoutActions$.mockImplementation(() =>
|
||||
of([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]),
|
||||
);
|
||||
mockSessionTimeoutSettingsComponentService.availableTimeoutOptions$ =
|
||||
availableTimeoutOptions$.asObservable();
|
||||
mockPolicyService.policiesByType$.mockImplementation(() => of([]));
|
||||
mockSessionTimeoutSettingsComponentService.policyFilteredTimeoutOptions$.mockImplementation(
|
||||
(userId) => availableTimeoutOptions$.asObservable(),
|
||||
);
|
||||
mockPolicyService.policiesByType$.mockReturnValue(policies$.asObservable());
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@ -102,6 +112,8 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
{ provide: SessionTimeoutTypeService, useValue: mockSessionTimeoutTypeService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(SessionTimeoutSettingsComponent, {
|
||||
@ -145,6 +157,83 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
describe("supportsLock", () => {
|
||||
it.each([ClientType.Desktop, ClientType.Browser, ClientType.Cli])(
|
||||
"should return true when client is %s and policy action is null",
|
||||
fakeAsync((clientType: ClientType) => {
|
||||
mockPlatformUtilsService.getClientType.mockReturnValue(clientType);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(component.supportsLock).toBe(true);
|
||||
}),
|
||||
);
|
||||
|
||||
it.each([ClientType.Desktop, ClientType.Browser, ClientType.Cli])(
|
||||
"should return true when client is %s and policy action is lock",
|
||||
fakeAsync((clientType: ClientType) => {
|
||||
mockPlatformUtilsService.getClientType.mockReturnValue(clientType);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
minutes: 15,
|
||||
action: VaultTimeoutAction.Lock,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component.supportsLock).toBe(true);
|
||||
}),
|
||||
);
|
||||
|
||||
it.each([ClientType.Desktop, ClientType.Browser, ClientType.Cli, ClientType.Web])(
|
||||
"should return false when client is %s and policy action is logOut",
|
||||
fakeAsync((clientType: ClientType) => {
|
||||
mockPlatformUtilsService.getClientType.mockReturnValue(clientType);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
minutes: 15,
|
||||
action: VaultTimeoutAction.LogOut,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component.supportsLock).toBe(false);
|
||||
}),
|
||||
);
|
||||
|
||||
it("should return false when client is Web and policy action is null", fakeAsync(() => {
|
||||
mockPlatformUtilsService.getClientType.mockReturnValue(ClientType.Web);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(component.supportsLock).toBe(false);
|
||||
}));
|
||||
|
||||
it("should return false when client is Web and policy action is lock", fakeAsync(() => {
|
||||
mockPlatformUtilsService.getClientType.mockReturnValue(ClientType.Web);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
minutes: 15,
|
||||
action: VaultTimeoutAction.Lock,
|
||||
};
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component.supportsLock).toBe(false);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
it("should initialize available timeout options", fakeAsync(async () => {
|
||||
fixture.detectChanges();
|
||||
@ -178,7 +267,7 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
});
|
||||
}));
|
||||
|
||||
it("should initialize available timeout actions", fakeAsync(() => {
|
||||
it("should initialize available timeout actions signal", fakeAsync(() => {
|
||||
const expectedActions = [VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut];
|
||||
|
||||
mockVaultTimeoutSettingsService.availableVaultTimeoutActions$.mockImplementation(() =>
|
||||
@ -209,27 +298,55 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
expect(component.formGroup.value.timeoutAction).toBe(expectedAction);
|
||||
}));
|
||||
|
||||
it("should fall back to OnRestart when current option is not available", fakeAsync(() => {
|
||||
availableTimeoutOptions$.next([
|
||||
{ name: "oneMinute-used-i18n", value: 1 },
|
||||
{ name: "fiveMinutes-used-i18n", value: 5 },
|
||||
{ name: "onRestart-used-i18n", value: VaultTimeoutStringType.OnRestart },
|
||||
]);
|
||||
it("should initialize userId from active account", fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
const unavailableTimeout = VaultTimeoutStringType.Never;
|
||||
expect(component["userId"]).toBe(mockUserId);
|
||||
}));
|
||||
|
||||
mockVaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockImplementation(() =>
|
||||
of(unavailableTimeout),
|
||||
);
|
||||
it("should initialize sessionTimeoutActionFromPolicy signal with null when no policy exists", fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(component["sessionTimeoutActionFromPolicy"]()).toBeNull();
|
||||
}));
|
||||
|
||||
it.each([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut])(
|
||||
"should initialize sessionTimeoutActionFromPolicy signal with policy action %s when policy exists",
|
||||
fakeAsync((timeoutAction: VaultTimeoutAction) => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
minutes: 15,
|
||||
action: timeoutAction,
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component["sessionTimeoutActionFromPolicy"]()).toBe(timeoutAction);
|
||||
}),
|
||||
);
|
||||
|
||||
it("should initialize sessionTimeoutActionFromPolicy signal with null when policy exists and action is user preference", fakeAsync(() => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
minutes: 15,
|
||||
action: null,
|
||||
};
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(component.formGroup.value.timeout).toBe(VaultTimeoutStringType.OnRestart);
|
||||
policies$.next([{ id: "1", enabled: true, data: policyData } as Policy]);
|
||||
flush();
|
||||
|
||||
expect(component["sessionTimeoutActionFromPolicy"]()).toBeNull();
|
||||
}));
|
||||
|
||||
it("should disable timeout action control when policy enforces action", fakeAsync(() => {
|
||||
const policyData: MaximumVaultTimeoutPolicyData = {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
minutes: 15,
|
||||
action: VaultTimeoutAction.LogOut,
|
||||
};
|
||||
@ -273,7 +390,7 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
|
||||
expect(component.formGroup.controls.timeoutAction.enabled).toBe(true);
|
||||
|
||||
const policyData: MaximumVaultTimeoutPolicyData = {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
minutes: 15,
|
||||
action: VaultTimeoutAction.LogOut,
|
||||
};
|
||||
@ -355,6 +472,56 @@ describe("SessionTimeoutSettingsComponent", () => {
|
||||
|
||||
expect(saveSpy).toHaveBeenCalledWith(VaultTimeoutAction.LogOut);
|
||||
}));
|
||||
|
||||
it("should sync form timeout when service emits new timeout value", fakeAsync(() => {
|
||||
const timeout$ = new BehaviorSubject<VaultTimeout>(mockInitialTimeout);
|
||||
mockVaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(timeout$);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(component.formGroup.controls.timeout.value).toBe(mockInitialTimeout);
|
||||
|
||||
const newTimeout = 30;
|
||||
timeout$.next(newTimeout);
|
||||
flush();
|
||||
|
||||
expect(component.formGroup.controls.timeout.value).toBe(newTimeout);
|
||||
}));
|
||||
|
||||
it("should not sync form timeout when service emits same timeout value", fakeAsync(() => {
|
||||
const timeout$ = new BehaviorSubject<VaultTimeout>(mockInitialTimeout);
|
||||
mockVaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(timeout$);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
const setValueSpy = jest.spyOn(component.formGroup.controls.timeout, "setValue");
|
||||
|
||||
timeout$.next(mockInitialTimeout);
|
||||
flush();
|
||||
|
||||
expect(setValueSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it("should update availableTimeoutActions signal when service emits new actions", fakeAsync(() => {
|
||||
const actions$ = new BehaviorSubject<VaultTimeoutAction[]>([VaultTimeoutAction.Lock]);
|
||||
mockVaultTimeoutSettingsService.availableVaultTimeoutActions$.mockReturnValue(actions$);
|
||||
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(component["availableTimeoutActions"]()).toEqual([VaultTimeoutAction.Lock]);
|
||||
|
||||
actions$.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]);
|
||||
refreshTimeoutActionSettings$.next(undefined);
|
||||
flush();
|
||||
|
||||
expect(component["availableTimeoutActions"]()).toEqual([
|
||||
VaultTimeoutAction.Lock,
|
||||
VaultTimeoutAction.LogOut,
|
||||
]);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("saveTimeout", () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, input, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Component, DestroyRef, inject, input, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
@ -18,27 +18,28 @@ import {
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
pairwise,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { MaximumSessionTimeoutPolicyData } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
MaximumVaultTimeoutPolicyData,
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
CheckboxModule,
|
||||
@ -86,6 +87,19 @@ export class SessionTimeoutSettingsComponent implements OnInit {
|
||||
new BehaviorSubject<void>(undefined),
|
||||
);
|
||||
|
||||
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
private readonly sessionTimeoutSettingsComponentService = inject(
|
||||
SessionTimeoutSettingsComponentService,
|
||||
);
|
||||
private readonly i18nService = inject(I18nService);
|
||||
private readonly toastService = inject(ToastService);
|
||||
private readonly policyService = inject(PolicyService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly dialogService = inject(DialogService);
|
||||
private readonly logService = inject(LogService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly platformUtilsService = inject(PlatformUtilsService);
|
||||
|
||||
formGroup = new FormGroup({
|
||||
timeout: new FormControl<VaultTimeout | null>(null, [Validators.required]),
|
||||
timeoutAction: new FormControl<VaultTimeoutAction>(VaultTimeoutAction.Lock, [
|
||||
@ -93,63 +107,48 @@ export class SessionTimeoutSettingsComponent implements OnInit {
|
||||
]),
|
||||
});
|
||||
protected readonly availableTimeoutActions = signal<VaultTimeoutAction[]>([]);
|
||||
protected readonly availableTimeoutOptions$ =
|
||||
this.sessionTimeoutSettingsComponentService.availableTimeoutOptions$.pipe(
|
||||
startWith([] as VaultTimeoutOption[]),
|
||||
);
|
||||
protected hasVaultTimeoutPolicy$: Observable<boolean> = of(false);
|
||||
protected readonly availableTimeoutOptions$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.sessionTimeoutSettingsComponentService.policyFilteredTimeoutOptions$(userId),
|
||||
),
|
||||
tap((options) => {
|
||||
this.logService.debug("[SessionTimeoutSettings] Available timeout options", options);
|
||||
}),
|
||||
);
|
||||
protected readonly sessionTimeoutActionFromPolicy$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.policyService.policiesByType$(PolicyType.MaximumVaultTimeout, userId),
|
||||
),
|
||||
getFirstPolicy,
|
||||
map((policy) => policy?.data as MaximumSessionTimeoutPolicyData | undefined),
|
||||
map((data) => data?.action ?? null),
|
||||
);
|
||||
protected readonly sessionTimeoutActionFromPolicy = toSignal(
|
||||
this.sessionTimeoutActionFromPolicy$,
|
||||
);
|
||||
|
||||
private userId!: UserId;
|
||||
|
||||
constructor(
|
||||
private readonly vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private readonly sessionTimeoutSettingsComponentService: SessionTimeoutSettingsComponentService,
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly toastService: ToastService,
|
||||
private readonly policyService: PolicyService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly logService: LogService,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
) {}
|
||||
|
||||
get canLock() {
|
||||
return this.availableTimeoutActions().includes(VaultTimeoutAction.Lock);
|
||||
}
|
||||
|
||||
get supportsLock() {
|
||||
return (
|
||||
this.platformUtilsService.getClientType() !== ClientType.Web &&
|
||||
this.sessionTimeoutActionFromPolicy() !== "logOut"
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const availableTimeoutOptions = await firstValueFrom(
|
||||
this.sessionTimeoutSettingsComponentService.availableTimeoutOptions$,
|
||||
);
|
||||
|
||||
this.logService.debug(
|
||||
"[SessionTimeoutSettings] Available timeout options",
|
||||
availableTimeoutOptions,
|
||||
);
|
||||
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const maximumVaultTimeoutPolicy$ = this.policyService
|
||||
.policiesByType$(PolicyType.MaximumVaultTimeout, this.userId)
|
||||
.pipe(getFirstPolicy);
|
||||
|
||||
this.hasVaultTimeoutPolicy$ = maximumVaultTimeoutPolicy$.pipe(map((policy) => policy != null));
|
||||
|
||||
let timeout = await firstValueFrom(
|
||||
const timeout = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(this.userId),
|
||||
);
|
||||
|
||||
// Fallback if current timeout option is not available on this platform
|
||||
// Only applies to string-based timeout types, not numeric values
|
||||
const hasCurrentOption = availableTimeoutOptions.some((opt) => opt.value === timeout);
|
||||
if (!hasCurrentOption && typeof timeout !== "number") {
|
||||
this.logService.debug(
|
||||
"[SessionTimeoutSettings] Current timeout option not available, falling back from",
|
||||
{ timeout },
|
||||
);
|
||||
timeout = VaultTimeoutStringType.OnRestart;
|
||||
}
|
||||
|
||||
this.formGroup.patchValue(
|
||||
{
|
||||
timeout: timeout,
|
||||
@ -160,6 +159,23 @@ export class SessionTimeoutSettingsComponent implements OnInit {
|
||||
{ emitEvent: false },
|
||||
);
|
||||
|
||||
// Sync form with reactive timeout updates to handle race condition where policies
|
||||
// load asynchronously and may override the initially set timeout value
|
||||
this.vaultTimeoutSettingsService
|
||||
.getVaultTimeoutByUserId$(this.userId)
|
||||
.pipe(
|
||||
filter((timeout) => this.formGroup.controls.timeout.value !== timeout),
|
||||
tap((timeout) =>
|
||||
this.logService.debug(
|
||||
`[SessionTimeoutSettings] Updating initial form timeout from ${this.formGroup.controls.timeout.value} to ${timeout}`,
|
||||
),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((timeout) => {
|
||||
this.formGroup.controls.timeout.setValue(timeout, { emitEvent: false });
|
||||
});
|
||||
|
||||
this.refreshTimeoutActionSettings()
|
||||
.pipe(
|
||||
startWith(undefined),
|
||||
@ -167,19 +183,17 @@ export class SessionTimeoutSettingsComponent implements OnInit {
|
||||
combineLatest([
|
||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(this.userId),
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(this.userId),
|
||||
maximumVaultTimeoutPolicy$,
|
||||
this.sessionTimeoutActionFromPolicy$,
|
||||
]),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe(([availableActions, action, policy]) => {
|
||||
.subscribe(([availableActions, action, sessionTimeoutActionFromPolicy]) => {
|
||||
this.availableTimeoutActions.set(availableActions);
|
||||
this.formGroup.controls.timeoutAction.setValue(action, { emitEvent: false });
|
||||
|
||||
const policyData = policy?.data as MaximumVaultTimeoutPolicyData | undefined;
|
||||
|
||||
// Enable/disable the action control based on policy or available actions
|
||||
if (policyData?.action != null || availableActions.length <= 1) {
|
||||
if (sessionTimeoutActionFromPolicy != null || availableActions.length <= 1) {
|
||||
this.formGroup.controls.timeoutAction.disable({ emitEvent: false });
|
||||
} else {
|
||||
this.formGroup.controls.timeoutAction.enable({ emitEvent: false });
|
||||
|
||||
@ -0,0 +1,337 @@
|
||||
import { fakeAsync, flush } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import {
|
||||
MaximumSessionTimeoutPolicyData,
|
||||
SessionTimeoutTypeService,
|
||||
} from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { SessionTimeoutSettingsComponentService } from "./session-timeout-settings-component.service";
|
||||
|
||||
describe("SessionTimeoutSettingsComponentService", () => {
|
||||
let service: SessionTimeoutSettingsComponentService;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockSessionTimeoutTypeService: MockProxy<SessionTimeoutTypeService>;
|
||||
let mockPolicyService: MockProxy<PolicyService>;
|
||||
|
||||
const mockUserId = "test-user-id" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockSessionTimeoutTypeService = mock<SessionTimeoutTypeService>();
|
||||
mockPolicyService = mock<PolicyService>();
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`);
|
||||
mockSessionTimeoutTypeService.isAvailable.mockResolvedValue(true);
|
||||
mockPolicyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
service = new SessionTimeoutSettingsComponentService(
|
||||
mockI18nService,
|
||||
mockSessionTimeoutTypeService,
|
||||
mockPolicyService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("availableTimeoutOptions$", () => {
|
||||
it("should return all options when isAvailable returns true for all", fakeAsync(async () => {
|
||||
mockSessionTimeoutTypeService.isAvailable.mockResolvedValue(true);
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service["availableTimeoutOptions$"]);
|
||||
|
||||
assertAllTimeoutTypes(options);
|
||||
}));
|
||||
|
||||
it("should filter options based on isAvailable() results", fakeAsync(async () => {
|
||||
mockSessionTimeoutTypeService.isAvailable.mockImplementation(async (value: VaultTimeout) => {
|
||||
return (
|
||||
value === VaultTimeoutNumberType.OnMinute ||
|
||||
value === 5 ||
|
||||
value === VaultTimeoutStringType.OnLocked
|
||||
);
|
||||
});
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service["availableTimeoutOptions$"]);
|
||||
|
||||
expect(options).toHaveLength(3);
|
||||
expect(options).toContainEqual({ name: "oneMinute", value: VaultTimeoutNumberType.OnMinute });
|
||||
expect(options).toContainEqual({ name: "fiveMinutes", value: 5 });
|
||||
expect(options).toContainEqual({ name: "onLocked", value: VaultTimeoutStringType.OnLocked });
|
||||
expect(options).not.toContainEqual({
|
||||
name: "immediately",
|
||||
value: VaultTimeoutNumberType.Immediately,
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe("policyFilteredTimeoutOptions$", () => {
|
||||
it("should return all available options when no policy for user", fakeAsync(async () => {
|
||||
mockPolicyService.policiesByType$.mockReturnValue(of([]));
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service.policyFilteredTimeoutOptions$(mockUserId));
|
||||
|
||||
assertAllTimeoutTypes(options);
|
||||
}));
|
||||
|
||||
describe('policy type "immediately"', () => {
|
||||
it.each([VaultTimeoutNumberType.Immediately, VaultTimeoutNumberType.OnMinute])(
|
||||
"should only return immediately option or fallback",
|
||||
fakeAsync(async (availableTimeoutOrPromoted: VaultTimeout) => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "immediately",
|
||||
minutes: 0,
|
||||
};
|
||||
const policy = {
|
||||
id: "policy-id",
|
||||
organizationId: "org-id",
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
data: policyData,
|
||||
enabled: true,
|
||||
} as Policy;
|
||||
|
||||
mockPolicyService.policiesByType$.mockReturnValue(of([policy]));
|
||||
mockSessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(
|
||||
availableTimeoutOrPromoted,
|
||||
);
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service.policyFilteredTimeoutOptions$(mockUserId));
|
||||
|
||||
expect(options).toHaveLength(1);
|
||||
if (availableTimeoutOrPromoted === VaultTimeoutNumberType.Immediately) {
|
||||
expect(options[0]).toEqual({
|
||||
name: "immediately",
|
||||
value: VaultTimeoutNumberType.Immediately,
|
||||
});
|
||||
} else {
|
||||
expect(options[0]).toEqual({
|
||||
name: "oneMinute",
|
||||
value: VaultTimeoutNumberType.OnMinute,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('policy type "onSystemLock"', () => {
|
||||
it.each([VaultTimeoutStringType.OnLocked, VaultTimeoutStringType.OnRestart])(
|
||||
"should allow immediately, numeric, custom, onLocked, onIdle, onSleep or fallback",
|
||||
fakeAsync(async (availableTimeoutOrPromoted: VaultTimeout) => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "onSystemLock",
|
||||
minutes: 0,
|
||||
};
|
||||
const policy = {
|
||||
id: "policy-id",
|
||||
organizationId: "org-id",
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
data: policyData,
|
||||
enabled: true,
|
||||
} as Policy;
|
||||
|
||||
mockPolicyService.policiesByType$.mockReturnValue(of([policy]));
|
||||
mockSessionTimeoutTypeService.getOrPromoteToAvailable.mockResolvedValue(
|
||||
availableTimeoutOrPromoted,
|
||||
);
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service.policyFilteredTimeoutOptions$(mockUserId));
|
||||
|
||||
assertNumericTimeoutTypes(options);
|
||||
expect(options).toContainEqual({
|
||||
name: "onLocked",
|
||||
value: VaultTimeoutStringType.OnLocked,
|
||||
});
|
||||
expect(options).toContainEqual({ name: "onIdle", value: VaultTimeoutStringType.OnIdle });
|
||||
expect(options).toContainEqual({
|
||||
name: "onSleep",
|
||||
value: VaultTimeoutStringType.OnSleep,
|
||||
});
|
||||
expect(options).toContainEqual({ name: "custom", value: VaultTimeoutStringType.Custom });
|
||||
expect(options).not.toContainEqual({
|
||||
name: "never",
|
||||
value: VaultTimeoutStringType.Never,
|
||||
});
|
||||
if (availableTimeoutOrPromoted === VaultTimeoutStringType.OnLocked) {
|
||||
expect(options).not.toContainEqual({
|
||||
name: "sessionTimeoutOnRestart",
|
||||
value: VaultTimeoutStringType.OnRestart,
|
||||
});
|
||||
} else {
|
||||
expect(options).toContainEqual({
|
||||
name: "sessionTimeoutOnRestart",
|
||||
value: VaultTimeoutStringType.OnRestart,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('policy type "onAppRestart"', () => {
|
||||
it("should allow immediately, numeric, custom, and onRestart", fakeAsync(async () => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "onAppRestart",
|
||||
minutes: 0,
|
||||
};
|
||||
const policy = {
|
||||
id: "policy-id",
|
||||
organizationId: "org-id",
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
data: policyData,
|
||||
enabled: true,
|
||||
} as Policy;
|
||||
|
||||
mockPolicyService.policiesByType$.mockReturnValue(of([policy]));
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service.policyFilteredTimeoutOptions$(mockUserId));
|
||||
|
||||
assertNumericTimeoutTypes(options);
|
||||
expect(options).toContainEqual({
|
||||
name: "sessionTimeoutOnRestart",
|
||||
value: VaultTimeoutStringType.OnRestart,
|
||||
});
|
||||
expect(options).toContainEqual({ name: "custom", value: VaultTimeoutStringType.Custom });
|
||||
expect(options).not.toContainEqual({
|
||||
name: "onLocked",
|
||||
value: VaultTimeoutStringType.OnLocked,
|
||||
});
|
||||
expect(options).not.toContainEqual({
|
||||
name: "onIdle",
|
||||
value: VaultTimeoutStringType.OnIdle,
|
||||
});
|
||||
expect(options).not.toContainEqual({
|
||||
name: "onSleep",
|
||||
value: VaultTimeoutStringType.OnSleep,
|
||||
});
|
||||
expect(options).not.toContainEqual({ name: "never", value: VaultTimeoutStringType.Never });
|
||||
}));
|
||||
});
|
||||
|
||||
describe('policy type "custom", null, or undefined', () => {
|
||||
it.each(["custom", null, undefined])(
|
||||
"should allow immediately, custom, and numeric values within policy limit when type is %s",
|
||||
fakeAsync(async (policyType: "custom" | null | undefined) => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: policyType as "custom" | null | undefined,
|
||||
minutes: 15,
|
||||
};
|
||||
const policy = {
|
||||
id: "policy-id",
|
||||
organizationId: "org-id",
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
data: policyData,
|
||||
enabled: true,
|
||||
} as Policy;
|
||||
|
||||
mockPolicyService.policiesByType$.mockReturnValue(of([policy]));
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service.policyFilteredTimeoutOptions$(mockUserId));
|
||||
|
||||
expect(options).toContainEqual({
|
||||
name: "immediately",
|
||||
value: VaultTimeoutNumberType.Immediately,
|
||||
});
|
||||
expect(options).toContainEqual({
|
||||
name: "oneMinute",
|
||||
value: VaultTimeoutNumberType.OnMinute,
|
||||
});
|
||||
expect(options).toContainEqual({ name: "fiveMinutes", value: 5 });
|
||||
expect(options).toContainEqual({ name: "fifteenMinutes", value: 15 });
|
||||
expect(options).toContainEqual({ name: "custom", value: VaultTimeoutStringType.Custom });
|
||||
expect(options).not.toContainEqual({ name: "thirtyMinutes", value: 30 });
|
||||
expect(options).not.toContainEqual({ name: "oneHour", value: 60 });
|
||||
expect(options).not.toContainEqual({ name: "fourHours", value: 240 });
|
||||
expect(options).not.toContainEqual({
|
||||
name: "onLocked",
|
||||
value: VaultTimeoutStringType.OnLocked,
|
||||
});
|
||||
expect(options).not.toContainEqual({
|
||||
name: "onIdle",
|
||||
value: VaultTimeoutStringType.OnIdle,
|
||||
});
|
||||
expect(options).not.toContainEqual({
|
||||
name: "onSleep",
|
||||
value: VaultTimeoutStringType.OnSleep,
|
||||
});
|
||||
expect(options).not.toContainEqual({
|
||||
name: "sessionTimeoutOnRestart",
|
||||
value: VaultTimeoutStringType.OnRestart,
|
||||
});
|
||||
expect(options).not.toContainEqual({
|
||||
name: "never",
|
||||
value: VaultTimeoutStringType.Never,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('policy type "never"', () => {
|
||||
it("should return all available options", fakeAsync(async () => {
|
||||
const policyData: MaximumSessionTimeoutPolicyData = {
|
||||
type: "never",
|
||||
minutes: 0,
|
||||
};
|
||||
const policy = {
|
||||
id: "policy-id",
|
||||
organizationId: "org-id",
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
data: policyData,
|
||||
enabled: true,
|
||||
} as Policy;
|
||||
|
||||
mockPolicyService.policiesByType$.mockReturnValue(of([policy]));
|
||||
flush();
|
||||
|
||||
const options = await firstValueFrom(service.policyFilteredTimeoutOptions$(mockUserId));
|
||||
|
||||
assertAllTimeoutTypes(options);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
function assertAllTimeoutTypes(options: VaultTimeoutOption[]) {
|
||||
assertNumericTimeoutTypes(options);
|
||||
expect(options).toContainEqual({ name: "onIdle", value: VaultTimeoutStringType.OnIdle });
|
||||
expect(options).toContainEqual({ name: "onSleep", value: VaultTimeoutStringType.OnSleep });
|
||||
expect(options).toContainEqual({ name: "onLocked", value: VaultTimeoutStringType.OnLocked });
|
||||
expect(options).toContainEqual({
|
||||
name: "sessionTimeoutOnRestart",
|
||||
value: VaultTimeoutStringType.OnRestart,
|
||||
});
|
||||
expect(options).toContainEqual({ name: "never", value: VaultTimeoutStringType.Never });
|
||||
expect(options).toContainEqual({ name: "custom", value: VaultTimeoutStringType.Custom });
|
||||
}
|
||||
|
||||
function assertNumericTimeoutTypes(options: VaultTimeoutOption[]) {
|
||||
expect(options).toContainEqual({
|
||||
name: "immediately",
|
||||
value: VaultTimeoutNumberType.Immediately,
|
||||
});
|
||||
expect(options).toContainEqual({ name: "oneMinute", value: VaultTimeoutNumberType.OnMinute });
|
||||
expect(options).toContainEqual({ name: "fiveMinutes", value: 5 });
|
||||
expect(options).toContainEqual({ name: "fifteenMinutes", value: 15 });
|
||||
expect(options).toContainEqual({ name: "thirtyMinutes", value: 30 });
|
||||
expect(options).toContainEqual({ name: "oneHour", value: 60 });
|
||||
expect(options).toContainEqual({ name: "fourHours", value: 240 });
|
||||
}
|
||||
});
|
||||
@ -1,9 +1,158 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { combineLatest, concatMap, defer, map, Observable } from "rxjs";
|
||||
|
||||
import { VaultTimeout, VaultTimeoutOption } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import {
|
||||
MaximumSessionTimeoutPolicyData,
|
||||
SessionTimeoutTypeService,
|
||||
} from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
isVaultTimeoutTypeNumeric,
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export abstract class SessionTimeoutSettingsComponentService {
|
||||
abstract availableTimeoutOptions$: Observable<VaultTimeoutOption[]>;
|
||||
export class SessionTimeoutSettingsComponentService {
|
||||
private readonly availableTimeoutOptions$: Observable<VaultTimeoutOption[]>;
|
||||
|
||||
abstract onTimeoutSave(timeout: VaultTimeout): void;
|
||||
constructor(
|
||||
protected readonly i18nService: I18nService,
|
||||
protected readonly sessionTimeoutTypeService: SessionTimeoutTypeService,
|
||||
protected readonly policyService: PolicyService,
|
||||
) {
|
||||
this.availableTimeoutOptions$ = defer(async () => {
|
||||
const allOptions = this.getAllTimeoutOptions();
|
||||
const availabilityResults = await Promise.all(
|
||||
allOptions.map(async (option) => ({
|
||||
option,
|
||||
available: await this.sessionTimeoutTypeService.isAvailable(option.value),
|
||||
})),
|
||||
);
|
||||
|
||||
return availabilityResults
|
||||
.filter((result) => result.available)
|
||||
.map((result) => result.option);
|
||||
});
|
||||
}
|
||||
|
||||
onTimeoutSave(_timeout: VaultTimeout): void {
|
||||
// Default implementation does nothing, but other clients might want to override this
|
||||
}
|
||||
|
||||
policyFilteredTimeoutOptions$(userId: UserId): Observable<VaultTimeoutOption[]> {
|
||||
const policyData$ = this.policyService
|
||||
.policiesByType$(PolicyType.MaximumVaultTimeout, userId)
|
||||
.pipe(
|
||||
getFirstPolicy,
|
||||
map((policy) => (policy?.data ?? null) as MaximumSessionTimeoutPolicyData | null),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
this.availableTimeoutOptions$,
|
||||
policyData$,
|
||||
policyData$.pipe(
|
||||
concatMap(async (policyData) => {
|
||||
if (policyData == null) {
|
||||
return null;
|
||||
}
|
||||
switch (policyData.type) {
|
||||
case "immediately":
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutNumberType.Immediately,
|
||||
);
|
||||
case "onSystemLock":
|
||||
return await this.sessionTimeoutTypeService.getOrPromoteToAvailable(
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
),
|
||||
]).pipe(
|
||||
concatMap(
|
||||
async ([availableOptions, policyData, highestAvailableEnforcedByPolicyTimeoutType]) => {
|
||||
if (policyData == null) {
|
||||
return availableOptions;
|
||||
}
|
||||
|
||||
return availableOptions.filter((option) => {
|
||||
switch (policyData.type) {
|
||||
case "immediately": {
|
||||
// Policy requires immediate lock.
|
||||
return option.value === highestAvailableEnforcedByPolicyTimeoutType;
|
||||
}
|
||||
|
||||
case "onSystemLock": {
|
||||
// Allow immediately, numeric values, custom, and any system lock-related options.
|
||||
if (
|
||||
option.value === VaultTimeoutNumberType.Immediately ||
|
||||
isVaultTimeoutTypeNumeric(option.value) ||
|
||||
option.value === VaultTimeoutStringType.Custom ||
|
||||
option.value === VaultTimeoutStringType.OnLocked ||
|
||||
option.value === VaultTimeoutStringType.OnIdle ||
|
||||
option.value === VaultTimeoutStringType.OnSleep
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// When on locked is not supported, fallback.
|
||||
return option.value === highestAvailableEnforcedByPolicyTimeoutType;
|
||||
}
|
||||
|
||||
case "onAppRestart":
|
||||
// Allow immediately, numeric values, custom, and on app restart
|
||||
return (
|
||||
option.value === VaultTimeoutNumberType.Immediately ||
|
||||
isVaultTimeoutTypeNumeric(option.value) ||
|
||||
option.value === VaultTimeoutStringType.Custom ||
|
||||
option.value === VaultTimeoutStringType.OnRestart
|
||||
);
|
||||
|
||||
case "custom":
|
||||
case null:
|
||||
case undefined:
|
||||
// Allow immediately, custom, and numeric values within policy limit
|
||||
return (
|
||||
option.value === VaultTimeoutNumberType.Immediately ||
|
||||
option.value === VaultTimeoutStringType.Custom ||
|
||||
(isVaultTimeoutTypeNumeric(option.value) &&
|
||||
(option.value as number) <= policyData.minutes)
|
||||
);
|
||||
|
||||
case "never":
|
||||
// No policy restriction
|
||||
return true;
|
||||
|
||||
default:
|
||||
throw Error(`Unsupported policy type: ${policyData.type}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private getAllTimeoutOptions(): VaultTimeoutOption[] {
|
||||
return [
|
||||
{ name: "immediately", value: VaultTimeoutNumberType.Immediately },
|
||||
{ name: "oneMinute", value: VaultTimeoutNumberType.OnMinute },
|
||||
{ name: "fiveMinutes", value: 5 },
|
||||
{ name: "fifteenMinutes", value: 15 },
|
||||
{ name: "thirtyMinutes", value: 30 },
|
||||
{ name: "oneHour", value: 60 },
|
||||
{ name: "fourHours", value: 240 },
|
||||
{ name: "onIdle", value: VaultTimeoutStringType.OnIdle },
|
||||
{ name: "onSleep", value: VaultTimeoutStringType.OnSleep },
|
||||
{ name: "onLocked", value: VaultTimeoutStringType.OnLocked },
|
||||
{ name: "sessionTimeoutOnRestart", value: VaultTimeoutStringType.OnRestart },
|
||||
{ name: "never", value: VaultTimeoutStringType.Never },
|
||||
{ name: "custom", value: VaultTimeoutStringType.Custom },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user