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:
@@ -11,7 +11,7 @@
|
|||||||
<h2 bitTypography="h6">{{ "unlockMethods" | i18n }}</h2>
|
<h2 bitTypography="h6">{{ "unlockMethods" | i18n }}</h2>
|
||||||
</bit-section-header>
|
</bit-section-header>
|
||||||
<bit-card>
|
<bit-card>
|
||||||
<bit-form-control>
|
<bit-form-control [disableMargin]="!((pinEnabled$ | async) || this.form.value.pin)">
|
||||||
<input bitCheckbox id="biometric" type="checkbox" formControlName="biometric" />
|
<input bitCheckbox id="biometric" type="checkbox" formControlName="biometric" />
|
||||||
<bit-label for="biometric" class="tw-whitespace-normal">{{
|
<bit-label for="biometric" class="tw-whitespace-normal">{{
|
||||||
"unlockWithBiometrics" | i18n
|
"unlockWithBiometrics" | i18n
|
||||||
@@ -20,7 +20,11 @@
|
|||||||
{{ biometricUnavailabilityReason }}
|
{{ biometricUnavailabilityReason }}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control class="tw-pl-5" *ngIf="this.form.value.biometric && showAutoPrompt">
|
<bit-form-control
|
||||||
|
class="tw-pl-5"
|
||||||
|
[disableMargin]="!((pinEnabled$ | async) || this.form.value.pin)"
|
||||||
|
*ngIf="this.form.value.biometric && showAutoPrompt"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
id="autoBiometricsPrompt"
|
id="autoBiometricsPrompt"
|
||||||
@@ -33,6 +37,7 @@
|
|||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
<bit-form-control
|
<bit-form-control
|
||||||
[disableMargin]="!(this.form.value.pin && showMasterPasswordOnClientRestartOption)"
|
[disableMargin]="!(this.form.value.pin && showMasterPasswordOnClientRestartOption)"
|
||||||
|
*ngIf="(pinEnabled$ | async) || this.form.value.pin"
|
||||||
>
|
>
|
||||||
<input bitCheckbox id="pin" type="checkbox" formControlName="pin" />
|
<input bitCheckbox id="pin" type="checkbox" formControlName="pin" />
|
||||||
<bit-label for="pin" class="tw-whitespace-normal">{{ "unlockWithPin" | i18n }}</bit-label>
|
<bit-label for="pin" class="tw-whitespace-normal">{{ "unlockWithPin" | i18n }}</bit-label>
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { Component } 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 { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.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 { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
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, ToastService } from "@bitwarden/components";
|
||||||
|
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
|
||||||
|
|
||||||
|
import { AccountSecurityComponent } from "./account-security.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "app-pop-out",
|
||||||
|
template: ` <ng-content></ng-content>`,
|
||||||
|
})
|
||||||
|
class MockPopOutComponent {}
|
||||||
|
|
||||||
|
describe("AccountSecurityComponent", () => {
|
||||||
|
let component: AccountSecurityComponent;
|
||||||
|
let fixture: ComponentFixture<AccountSecurityComponent>;
|
||||||
|
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
|
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||||
|
const biometricStateService = mock<BiometricStateService>();
|
||||||
|
const policyService = mock<PolicyService>();
|
||||||
|
const pinServiceAbstraction = mock<PinServiceAbstraction>();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: AccountService, useValue: accountService },
|
||||||
|
{ provide: AccountSecurityComponent, useValue: mock<AccountSecurityComponent>() },
|
||||||
|
{ provide: BiometricsService, useValue: mock<BiometricsService>() },
|
||||||
|
{ provide: BiometricStateService, useValue: biometricStateService },
|
||||||
|
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||||
|
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
|
||||||
|
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||||
|
{ provide: MessageSender, useValue: mock<MessageSender>() },
|
||||||
|
{ provide: KeyService, useValue: mock<KeyService>() },
|
||||||
|
{ provide: PinServiceAbstraction, useValue: pinServiceAbstraction },
|
||||||
|
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||||
|
{ provide: PolicyService, useValue: policyService },
|
||||||
|
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||||
|
{ provide: StateService, useValue: mock<StateService>() },
|
||||||
|
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||||
|
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||||
|
{ provide: VaultTimeoutService, useValue: mock<VaultTimeoutService>() },
|
||||||
|
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.overrideComponent(AccountSecurityComponent, {
|
||||||
|
remove: {
|
||||||
|
imports: [PopOutComponent],
|
||||||
|
},
|
||||||
|
add: {
|
||||||
|
imports: [MockPopOutComponent],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AccountSecurityComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
|
||||||
|
of(VaultTimeoutStringType.OnLocked),
|
||||||
|
);
|
||||||
|
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
|
||||||
|
of(VaultTimeoutAction.Lock),
|
||||||
|
);
|
||||||
|
biometricStateService.promptAutomatically$ = of(false);
|
||||||
|
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||||
|
expect(pinInputElement).not.toBeNull();
|
||||||
|
expect(pinInputElement.name).toBe("input");
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||||
|
expect(pinInputElement).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
pairwise,
|
pairwise,
|
||||||
startWith,
|
startWith,
|
||||||
Subject,
|
Subject,
|
||||||
@@ -108,6 +110,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
biometricUnavailabilityReason: string;
|
biometricUnavailabilityReason: string;
|
||||||
showChangeMasterPass = true;
|
showChangeMasterPass = true;
|
||||||
showAutoPrompt = true;
|
showAutoPrompt = true;
|
||||||
|
pinEnabled$: Observable<boolean> = of(true);
|
||||||
|
|
||||||
form = this.formBuilder.group({
|
form = this.formBuilder.group({
|
||||||
vaultTimeout: [null as VaultTimeout | null],
|
vaultTimeout: [null as VaultTimeout | null],
|
||||||
@@ -193,6 +196,12 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
|||||||
timeout = VaultTimeoutStringType.OnRestart;
|
timeout = VaultTimeoutStringType.OnRestart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pinEnabled$ = this.policyService.get$(PolicyType.RemoveUnlockWithPin).pipe(
|
||||||
|
map((policy) => {
|
||||||
|
return policy == null || !policy.enabled;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
vaultTimeout: timeout,
|
vaultTimeout: timeout,
|
||||||
vaultTimeoutAction: await firstValueFrom(
|
vaultTimeoutAction: await firstValueFrom(
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
}}</small>
|
}}</small>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="form-group">
|
<div class="form-group" *ngIf="(pinEnabled$ | async) || this.form.value.pin">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label for="pin">
|
<label for="pin">
|
||||||
<input id="pin" type="checkbox" formControlName="pin" />
|
<input id="pin" type="checkbox" formControlName="pin" />
|
||||||
@@ -163,7 +163,11 @@
|
|||||||
formControlName="requirePasswordOnStart"
|
formControlName="requirePasswordOnStart"
|
||||||
(change)="updateRequirePasswordOnStart()"
|
(change)="updateRequirePasswordOnStart()"
|
||||||
/>
|
/>
|
||||||
{{ "requirePasswordOnStart" | i18n }}
|
@if (pinEnabled$ | async) {
|
||||||
|
{{ "requirePasswordOnStart" | i18n }}
|
||||||
|
} @else {
|
||||||
|
{{ "requirePasswordWithoutPinOnStart" | i18n }}
|
||||||
|
}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<small class="help-block form-group-child" *ngIf="isWindows">{{
|
<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
|
// @ts-strict-ignore
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, Observable, Subject, firstValueFrom, of } from "rxjs";
|
||||||
import {
|
import {
|
||||||
concatMap,
|
concatMap,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@@ -99,6 +99,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||||||
userHasMasterPassword: boolean;
|
userHasMasterPassword: boolean;
|
||||||
userHasPinSet: boolean;
|
userHasPinSet: boolean;
|
||||||
|
|
||||||
|
pinEnabled$: Observable<boolean> = of(true);
|
||||||
|
|
||||||
form = this.formBuilder.group({
|
form = this.formBuilder.group({
|
||||||
// Security
|
// Security
|
||||||
vaultTimeout: [null as VaultTimeout | null],
|
vaultTimeout: [null as VaultTimeout | null],
|
||||||
@@ -262,6 +264,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||||||
// Load initial values
|
// Load initial values
|
||||||
this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id);
|
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 = {
|
const initialValues = {
|
||||||
vaultTimeout: await firstValueFrom(
|
vaultTimeout: await firstValueFrom(
|
||||||
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
|
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ export class DesktopAutofillSettingsService {
|
|||||||
private enableDuckDuckGoBrowserIntegrationState = this.stateProvider.getGlobal(
|
private enableDuckDuckGoBrowserIntegrationState = this.stateProvider.getGlobal(
|
||||||
ENABLE_DUCK_DUCK_GO_BROWSER_INTEGRATION,
|
ENABLE_DUCK_DUCK_GO_BROWSER_INTEGRATION,
|
||||||
);
|
);
|
||||||
readonly enableDuckDuckGoBrowserIntegration$ =
|
enableDuckDuckGoBrowserIntegration$ = this.enableDuckDuckGoBrowserIntegrationState.state$.pipe(
|
||||||
this.enableDuckDuckGoBrowserIntegrationState.state$.pipe(map((x) => x ?? false));
|
map((x) => x ?? false),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {}
|
constructor(private stateProvider: StateProvider) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1775,6 +1775,9 @@
|
|||||||
"requirePasswordOnStart": {
|
"requirePasswordOnStart": {
|
||||||
"message": "Require password or PIN on app start"
|
"message": "Require password or PIN on app start"
|
||||||
},
|
},
|
||||||
|
"requirePasswordWithoutPinOnStart": {
|
||||||
|
"message": "Require password on app start"
|
||||||
|
},
|
||||||
"recommendedForSecurity": {
|
"recommendedForSecurity": {
|
||||||
"message": "Recommended for security."
|
"message": "Recommended for security."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,18 +35,6 @@ export abstract class BasePolicyComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData() {
|
|
||||||
this.data.patchValue(this.policyResponse.data ?? {});
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRequestData() {
|
|
||||||
if (this.data != null) {
|
|
||||||
return this.data.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRequest() {
|
buildRequest() {
|
||||||
const request = new PolicyRequest();
|
const request = new PolicyRequest();
|
||||||
request.enabled = this.enabled.value;
|
request.enabled = this.enabled.value;
|
||||||
@@ -55,4 +43,16 @@ export abstract class BasePolicyComponent implements OnInit {
|
|||||||
|
|
||||||
return Promise.resolve(request);
|
return Promise.resolve(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected loadData() {
|
||||||
|
this.data.patchValue(this.policyResponse.data ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildRequestData() {
|
||||||
|
if (this.data != null) {
|
||||||
|
return this.data.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export { SendOptionsPolicy } from "./send-options.component";
|
|||||||
export { SingleOrgPolicy } from "./single-org.component";
|
export { SingleOrgPolicy } from "./single-org.component";
|
||||||
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
|
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
|
||||||
export { PoliciesComponent } from "./policies.component";
|
export { PoliciesComponent } from "./policies.component";
|
||||||
|
export { RemoveUnlockWithPinPolicy } from "./remove-unlock-with-pin.component";
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PasswordGeneratorPolicyComponent } from "./password-generator.component
|
|||||||
import { PersonalOwnershipPolicyComponent } from "./personal-ownership.component";
|
import { PersonalOwnershipPolicyComponent } from "./personal-ownership.component";
|
||||||
import { PoliciesComponent } from "./policies.component";
|
import { PoliciesComponent } from "./policies.component";
|
||||||
import { PolicyEditComponent } from "./policy-edit.component";
|
import { PolicyEditComponent } from "./policy-edit.component";
|
||||||
|
import { RemoveUnlockWithPinPolicyComponent } from "./remove-unlock-with-pin.component";
|
||||||
import { RequireSsoPolicyComponent } from "./require-sso.component";
|
import { RequireSsoPolicyComponent } from "./require-sso.component";
|
||||||
import { ResetPasswordPolicyComponent } from "./reset-password.component";
|
import { ResetPasswordPolicyComponent } from "./reset-password.component";
|
||||||
import { SendOptionsPolicyComponent } from "./send-options.component";
|
import { SendOptionsPolicyComponent } from "./send-options.component";
|
||||||
@@ -28,6 +29,7 @@ import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authenticat
|
|||||||
TwoFactorAuthenticationPolicyComponent,
|
TwoFactorAuthenticationPolicyComponent,
|
||||||
PoliciesComponent,
|
PoliciesComponent,
|
||||||
PolicyEditComponent,
|
PolicyEditComponent,
|
||||||
|
RemoveUnlockWithPinPolicyComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
DisableSendPolicyComponent,
|
DisableSendPolicyComponent,
|
||||||
@@ -41,6 +43,7 @@ import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authenticat
|
|||||||
TwoFactorAuthenticationPolicyComponent,
|
TwoFactorAuthenticationPolicyComponent,
|
||||||
PoliciesComponent,
|
PoliciesComponent,
|
||||||
PolicyEditComponent,
|
PolicyEditComponent,
|
||||||
|
RemoveUnlockWithPinPolicyComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PoliciesModule {}
|
export class PoliciesModule {}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<bit-form-control>
|
||||||
|
<input type="checkbox" id="enabled" bitCheckbox [formControl]="enabled" />
|
||||||
|
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||||
|
</bit-form-control>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { By } from "@angular/platform-browser";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RemoveUnlockWithPinPolicy,
|
||||||
|
RemoveUnlockWithPinPolicyComponent,
|
||||||
|
} from "./remove-unlock-with-pin.component";
|
||||||
|
|
||||||
|
describe("RemoveUnlockWithPinPolicy", () => {
|
||||||
|
const policy = new RemoveUnlockWithPinPolicy();
|
||||||
|
|
||||||
|
it("should have correct attributes", () => {
|
||||||
|
expect(policy.name).toEqual("removeUnlockWithPinPolicyTitle");
|
||||||
|
expect(policy.description).toEqual("removeUnlockWithPinPolicyDesc");
|
||||||
|
expect(policy.type).toEqual(PolicyType.RemoveUnlockWithPin);
|
||||||
|
expect(policy.component).toEqual(RemoveUnlockWithPinPolicyComponent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RemoveUnlockWithPinPolicyComponent", () => {
|
||||||
|
let component: RemoveUnlockWithPinPolicyComponent;
|
||||||
|
let fixture: ComponentFixture<RemoveUnlockWithPinPolicyComponent>;
|
||||||
|
const i18nService = mock<I18nService>();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, ReactiveFormsModule],
|
||||||
|
declarations: [RemoveUnlockWithPinPolicyComponent, I18nPipe],
|
||||||
|
providers: [
|
||||||
|
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||||
|
{ provide: I18nService, useValue: i18nService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RemoveUnlockWithPinPolicyComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("input selected on load when policy enabled", async () => {
|
||||||
|
component.policyResponse = new PolicyResponse({
|
||||||
|
id: "policy1",
|
||||||
|
organizationId: "org1",
|
||||||
|
type: PolicyType.RemoveUnlockWithPin,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.enabled.value).toBe(true);
|
||||||
|
const inputElement = fixture.debugElement.query(By.css("input"));
|
||||||
|
expect(inputElement).not.toBeNull();
|
||||||
|
expect(inputElement.properties).toMatchObject({
|
||||||
|
id: "enabled",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("input not selected on load when policy disabled", async () => {
|
||||||
|
component.policyResponse = new PolicyResponse({
|
||||||
|
id: "policy1",
|
||||||
|
organizationId: "org1",
|
||||||
|
type: PolicyType.RemoveUnlockWithPin,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(component.enabled.value).toBe(false);
|
||||||
|
const inputElement = fixture.debugElement.query(By.css("input"));
|
||||||
|
expect(inputElement).not.toBeNull();
|
||||||
|
expect(inputElement.properties).toMatchObject({
|
||||||
|
id: "enabled",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("turn on message label", async () => {
|
||||||
|
component.policyResponse = new PolicyResponse({
|
||||||
|
id: "policy1",
|
||||||
|
organizationId: "org1",
|
||||||
|
type: PolicyType.RemoveUnlockWithPin,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
i18nService.t.mockReturnValue("Turn on");
|
||||||
|
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const bitLabelElement = fixture.debugElement.query(By.css("bit-label"));
|
||||||
|
expect(bitLabelElement).not.toBeNull();
|
||||||
|
const textNodes = bitLabelElement.childNodes
|
||||||
|
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
|
||||||
|
.map((node) => node.nativeNode.wholeText?.trim());
|
||||||
|
expect(textNodes).toContain("Turn on");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
|
||||||
|
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||||
|
|
||||||
|
export class RemoveUnlockWithPinPolicy extends BasePolicy {
|
||||||
|
name = "removeUnlockWithPinPolicyTitle";
|
||||||
|
description = "removeUnlockWithPinPolicyDesc";
|
||||||
|
type = PolicyType.RemoveUnlockWithPin;
|
||||||
|
component = RemoveUnlockWithPinPolicyComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "remove-unlock-with-pin",
|
||||||
|
templateUrl: "remove-unlock-with-pin.component.html",
|
||||||
|
})
|
||||||
|
export class RemoveUnlockWithPinPolicyComponent extends BasePolicyComponent {}
|
||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
SendOptionsPolicy,
|
SendOptionsPolicy,
|
||||||
SingleOrgPolicy,
|
SingleOrgPolicy,
|
||||||
TwoFactorAuthenticationPolicy,
|
TwoFactorAuthenticationPolicy,
|
||||||
|
RemoveUnlockWithPinPolicy,
|
||||||
} from "./admin-console/organizations/policies";
|
} from "./admin-console/organizations/policies";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "AppComponent";
|
const BroadcasterSubscriptionId = "AppComponent";
|
||||||
@@ -255,6 +256,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.policyListService.addPolicies([
|
this.policyListService.addPolicies([
|
||||||
new TwoFactorAuthenticationPolicy(),
|
new TwoFactorAuthenticationPolicy(),
|
||||||
new MasterPasswordPolicy(),
|
new MasterPasswordPolicy(),
|
||||||
|
new RemoveUnlockWithPinPolicy(),
|
||||||
new ResetPasswordPolicy(),
|
new ResetPasswordPolicy(),
|
||||||
new PasswordGeneratorPolicy(),
|
new PasswordGeneratorPolicy(),
|
||||||
new SingleOrgPolicy(),
|
new SingleOrgPolicy(),
|
||||||
|
|||||||
@@ -10455,5 +10455,11 @@
|
|||||||
},
|
},
|
||||||
"assignedExceedsAvailable": {
|
"assignedExceedsAvailable": {
|
||||||
"message": "Assigned seats exceed available seats."
|
"message": "Assigned seats exceed available seats."
|
||||||
|
},
|
||||||
|
"removeUnlockWithPinPolicyTitle": {
|
||||||
|
"message": "Remove Unlock with PIN"
|
||||||
|
},
|
||||||
|
"removeUnlockWithPinPolicyDesc": {
|
||||||
|
"message": "Do not allow members to unlock their account with a PIN."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData() {
|
protected loadData() {
|
||||||
const minutes = this.policyResponse.data?.minutes;
|
const minutes = this.policyResponse.data?.minutes;
|
||||||
const action = this.policyResponse.data?.action;
|
const action = this.policyResponse.data?.action;
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buildRequestData() {
|
protected buildRequestData() {
|
||||||
if (this.data.value.hours == null && this.data.value.minutes == null) {
|
if (this.data.value.hours == null && this.data.value.minutes == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums/policy-type.enum";
|
||||||
|
|
||||||
|
describe("PolicyType", () => {
|
||||||
|
it("RemoveUnlockWithPin should be 14", () => {
|
||||||
|
expect(PolicyType.RemoveUnlockWithPin).toBe(14);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,4 +13,5 @@ export enum PolicyType {
|
|||||||
ActivateAutofill = 11, // Activates autofill with page load on the browser extension
|
ActivateAutofill = 11, // Activates autofill with page load on the browser extension
|
||||||
AutomaticAppLogIn = 12, // Enables automatic log in of apps from configured identity provider
|
AutomaticAppLogIn = 12, // Enables automatic log in of apps from configured identity provider
|
||||||
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
|
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
|
||||||
|
RemoveUnlockWithPin = 14, // Do not allow members to unlock their account with a PIN.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,19 +220,34 @@ describe("PolicyService", () => {
|
|||||||
arrayToRecord([
|
arrayToRecord([
|
||||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||||
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true),
|
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||||
|
policyData("policy3", "org1", PolicyType.RemoveUnlockWithPin, true),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
await expect(
|
||||||
policyService.get$(PolicyType.DisablePersonalVaultExport),
|
firstValueFrom(policyService.get$(PolicyType.ActivateAutofill)),
|
||||||
);
|
).resolves.toMatchObject({
|
||||||
|
id: "policy1",
|
||||||
expect(result).toEqual({
|
organizationId: "org1",
|
||||||
|
type: PolicyType.ActivateAutofill,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
firstValueFrom(policyService.get$(PolicyType.DisablePersonalVaultExport)),
|
||||||
|
).resolves.toMatchObject({
|
||||||
id: "policy2",
|
id: "policy2",
|
||||||
organizationId: "org1",
|
organizationId: "org1",
|
||||||
type: PolicyType.DisablePersonalVaultExport,
|
type: PolicyType.DisablePersonalVaultExport,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
await expect(
|
||||||
|
firstValueFrom(policyService.get$(PolicyType.RemoveUnlockWithPin)),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
id: "policy3",
|
||||||
|
organizationId: "org1",
|
||||||
|
type: PolicyType.RemoveUnlockWithPin,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not return disabled policies", async () => {
|
it("does not return disabled policies", async () => {
|
||||||
|
|||||||
@@ -247,6 +247,9 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
|
|||||||
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
case PolicyType.FreeFamiliesSponsorshipPolicy:
|
||||||
// free Bitwarden families policy applies to everyone
|
// free Bitwarden families policy applies to everyone
|
||||||
return false;
|
return false;
|
||||||
|
case PolicyType.RemoveUnlockWithPin:
|
||||||
|
// free Remove Unlock with PIN policy applies to everyone
|
||||||
|
return false;
|
||||||
default:
|
default:
|
||||||
return organization.canManagePolicies;
|
return organization.canManagePolicies;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user