mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-15882] Remove unlock with PIN policy (#13352)
* Remove policy with PIN in Web Vault * Remove policy with PIN in Browser Extension * Remove policy with PIN in Desktop * Remove policy with PIN in Desktop * unit tests coverage * unit tests coverage * unit tests coverage * private access method error * private access method error * private access method error * PM-18498: Unlock Options Padding Off When PIN Is Removed * PM-18498: Unlock Options Padding Off When PIN Is Removed
This commit is contained in:
@@ -111,7 +111,7 @@
|
||||
}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="(pinEnabled$ | async) || this.form.value.pin">
|
||||
<div class="checkbox">
|
||||
<label for="pin">
|
||||
<input id="pin" type="checkbox" formControlName="pin" />
|
||||
@@ -163,7 +163,11 @@
|
||||
formControlName="requirePasswordOnStart"
|
||||
(change)="updateRequirePasswordOnStart()"
|
||||
/>
|
||||
{{ "requirePasswordOnStart" | i18n }}
|
||||
@if (pinEnabled$ | async) {
|
||||
{{ "requirePasswordOnStart" | i18n }}
|
||||
} @else {
|
||||
{{ "requirePasswordWithoutPinOnStart" | i18n }}
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
<small class="help-block form-group-child" *ngIf="isWindows">{{
|
||||
|
||||
322
apps/desktop/src/app/accounts/settings.component.spec.ts
Normal file
322
apps/desktop/src/app/accounts/settings.component.spec.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { BiometricStateService, BiometricsStatus, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service";
|
||||
|
||||
import { SettingsComponent } from "./settings.component";
|
||||
|
||||
describe("SettingsComponent", () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
let originalIpc: any;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const policyService = mock<PolicyService>();
|
||||
const i18nService = mock<I18nService>();
|
||||
const autofillSettingsServiceAbstraction = mock<AutofillSettingsServiceAbstraction>();
|
||||
const desktopSettingsService = mock<DesktopSettingsService>();
|
||||
const domainSettingsService = mock<DomainSettingsService>();
|
||||
const desktopAutofillSettingsService = mock<DesktopAutofillSettingsService>();
|
||||
const themeStateService = mock<ThemeStateService>();
|
||||
const pinServiceAbstraction = mock<PinServiceAbstraction>();
|
||||
const desktopBiometricsService = mock<DesktopBiometricsService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
beforeEach(async () => {
|
||||
originalIpc = (global as any).ipc;
|
||||
(global as any).ipc = {
|
||||
auth: {
|
||||
loginRequest: jest.fn(),
|
||||
},
|
||||
platform: {
|
||||
isDev: false,
|
||||
isWindowsStore: false,
|
||||
powermonitor: {
|
||||
isLockMonitorAvailable: async () => false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
i18nService.supportedTranslationLocales = [];
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SettingsComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: AutofillSettingsServiceAbstraction,
|
||||
useValue: autofillSettingsServiceAbstraction,
|
||||
},
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: BiometricStateService, useValue: biometricStateService },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{
|
||||
provide: DesktopAutofillSettingsService,
|
||||
useValue: desktopAutofillSettingsService,
|
||||
},
|
||||
{ provide: DesktopBiometricsService, useValue: desktopBiometricsService },
|
||||
{ provide: DesktopSettingsService, useValue: desktopSettingsService },
|
||||
{ provide: DomainSettingsService, useValue: domainSettingsService },
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: MessageSender, useValue: mock<MessageSender>() },
|
||||
{
|
||||
provide: NativeMessagingManifestService,
|
||||
useValue: mock<NativeMessagingManifestService>(),
|
||||
},
|
||||
{ provide: KeyService, useValue: mock<KeyService>() },
|
||||
{ provide: PinServiceAbstraction, useValue: pinServiceAbstraction },
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: StateService, useValue: mock<StateService>() },
|
||||
{ provide: ThemeStateService, useValue: themeStateService },
|
||||
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
|
||||
of(VaultTimeoutStringType.OnLocked),
|
||||
);
|
||||
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
|
||||
of(VaultTimeoutAction.Lock),
|
||||
);
|
||||
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(false);
|
||||
biometricStateService.promptAutomatically$ = of(false);
|
||||
biometricStateService.requirePasswordOnStart$ = of(false);
|
||||
autofillSettingsServiceAbstraction.clearClipboardDelay$ = of(null);
|
||||
desktopSettingsService.minimizeOnCopy$ = of(false);
|
||||
desktopSettingsService.trayEnabled$ = of(false);
|
||||
desktopSettingsService.minimizeToTray$ = of(false);
|
||||
desktopSettingsService.closeToTray$ = of(false);
|
||||
desktopSettingsService.startToTray$ = of(false);
|
||||
desktopSettingsService.openAtLogin$ = of(false);
|
||||
desktopSettingsService.alwaysShowDock$ = of(false);
|
||||
desktopSettingsService.browserIntegrationEnabled$ = of(false);
|
||||
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(false);
|
||||
desktopSettingsService.hardwareAcceleration$ = of(false);
|
||||
desktopSettingsService.sshAgentEnabled$ = of(false);
|
||||
desktopSettingsService.preventScreenshots$ = of(false);
|
||||
domainSettingsService.showFavicons$ = of(false);
|
||||
desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$ = of(false);
|
||||
themeStateService.selectedTheme$ = of(ThemeType.System);
|
||||
i18nService.userSetLocale$ = of("en");
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(global as any).ipc = originalIpc;
|
||||
});
|
||||
|
||||
it("pin enabled when RemoveUnlockWithPin policy is not set", async () => {
|
||||
// @ts-strict-ignore
|
||||
policyService.get$.mockReturnValue(of(null));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("pin enabled when RemoveUnlockWithPin policy is disabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = false;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("pin disabled when RemoveUnlockWithPin policy is enabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("pin visible when RemoveUnlockWithPin policy is not set", async () => {
|
||||
// @ts-strict-ignore
|
||||
policyService.get$.mockReturnValue(of(null));
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).not.toBeNull();
|
||||
expect(pinInputElement.name).toBe("input");
|
||||
expect(pinInputElement.attributes).toMatchObject({
|
||||
type: "checkbox",
|
||||
});
|
||||
});
|
||||
|
||||
it("pin visible when RemoveUnlockWithPin policy is disabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = false;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).not.toBeNull();
|
||||
expect(pinInputElement.name).toBe("input");
|
||||
expect(pinInputElement.attributes).toMatchObject({
|
||||
type: "checkbox",
|
||||
});
|
||||
});
|
||||
|
||||
it("pin visible when RemoveUnlockWithPin policy is enabled and pin set", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).not.toBeNull();
|
||||
expect(pinInputElement.name).toBe("input");
|
||||
expect(pinInputElement.attributes).toMatchObject({
|
||||
type: "checkbox",
|
||||
});
|
||||
});
|
||||
|
||||
it("pin not visible when RemoveUnlockWithPin policy is enabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).toBeNull();
|
||||
});
|
||||
|
||||
describe("biometrics enabled", () => {
|
||||
beforeEach(() => {
|
||||
desktopBiometricsService.getBiometricsStatus.mockResolvedValue(BiometricsStatus.Available);
|
||||
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("require password or pin on app start message when RemoveUnlockWithPin policy is disabled and pin set and windows desktop", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = false;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
i18nService.t.mockImplementation((id: string) => {
|
||||
if (id === "requirePasswordOnStart") {
|
||||
return "Require password or pin on app start";
|
||||
} else if (id === "requirePasswordWithoutPinOnStart") {
|
||||
return "Require password on app start";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='requirePasswordOnStart']"),
|
||||
);
|
||||
expect(requirePasswordOnStartLabelElement).not.toBeNull();
|
||||
expect(requirePasswordOnStartLabelElement.children).toHaveLength(1);
|
||||
expect(requirePasswordOnStartLabelElement.children[0].name).toBe("input");
|
||||
expect(requirePasswordOnStartLabelElement.children[0].attributes).toMatchObject({
|
||||
id: "requirePasswordOnStart",
|
||||
type: "checkbox",
|
||||
});
|
||||
const textNodes = requirePasswordOnStartLabelElement.childNodes
|
||||
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.nativeNode.wholeText?.trim());
|
||||
expect(textNodes).toContain("Require password or pin on app start");
|
||||
});
|
||||
|
||||
it("require password on app start message when RemoveUnlockWithPin policy is enabled and pin set and windows desktop", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
i18nService.t.mockImplementation((id: string) => {
|
||||
if (id === "requirePasswordOnStart") {
|
||||
return "Require password or pin on app start";
|
||||
} else if (id === "requirePasswordWithoutPinOnStart") {
|
||||
return "Require password on app start";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='requirePasswordOnStart']"),
|
||||
);
|
||||
expect(requirePasswordOnStartLabelElement).not.toBeNull();
|
||||
expect(requirePasswordOnStartLabelElement.children).toHaveLength(1);
|
||||
expect(requirePasswordOnStartLabelElement.children[0].name).toBe("input");
|
||||
expect(requirePasswordOnStartLabelElement.children[0].attributes).toMatchObject({
|
||||
id: "requirePasswordOnStart",
|
||||
type: "checkbox",
|
||||
});
|
||||
const textNodes = requirePasswordOnStartLabelElement.childNodes
|
||||
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.nativeNode.wholeText?.trim());
|
||||
expect(textNodes).toContain("Require password on app start");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
|
||||
import { BehaviorSubject, Observable, Subject, firstValueFrom, of } from "rxjs";
|
||||
import {
|
||||
concatMap,
|
||||
debounceTime,
|
||||
@@ -99,6 +99,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
userHasMasterPassword: boolean;
|
||||
userHasPinSet: boolean;
|
||||
|
||||
pinEnabled$: Observable<boolean> = of(true);
|
||||
|
||||
form = this.formBuilder.group({
|
||||
// Security
|
||||
vaultTimeout: [null as VaultTimeout | null],
|
||||
@@ -262,6 +264,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
// Load initial values
|
||||
this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id);
|
||||
|
||||
this.pinEnabled$ = this.policyService.get$(PolicyType.RemoveUnlockWithPin).pipe(
|
||||
map((policy) => {
|
||||
return policy == null || !policy.enabled;
|
||||
}),
|
||||
);
|
||||
|
||||
const initialValues = {
|
||||
vaultTimeout: await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
|
||||
|
||||
@@ -18,8 +18,9 @@ export class DesktopAutofillSettingsService {
|
||||
private enableDuckDuckGoBrowserIntegrationState = this.stateProvider.getGlobal(
|
||||
ENABLE_DUCK_DUCK_GO_BROWSER_INTEGRATION,
|
||||
);
|
||||
readonly enableDuckDuckGoBrowserIntegration$ =
|
||||
this.enableDuckDuckGoBrowserIntegrationState.state$.pipe(map((x) => x ?? false));
|
||||
enableDuckDuckGoBrowserIntegration$ = this.enableDuckDuckGoBrowserIntegrationState.state$.pipe(
|
||||
map((x) => x ?? false),
|
||||
);
|
||||
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
|
||||
@@ -1775,6 +1775,9 @@
|
||||
"requirePasswordOnStart": {
|
||||
"message": "Require password or PIN on app start"
|
||||
},
|
||||
"requirePasswordWithoutPinOnStart": {
|
||||
"message": "Require password on app start"
|
||||
},
|
||||
"recommendedForSecurity": {
|
||||
"message": "Recommended for security."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user