8000 [PM-18049] Implemented Remove Unlock with Pin policy logic by fedemkr · Pull Request #1342 · bitwarden/ios · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[PM-18049] Implemented Remove Unlock with Pin policy logic #1342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ class DefaultAuthRepository {
/// The service used by the application to make organization user-related API requests.
private let organizationUserAPIService: OrganizationUserAPIService

/// The service used by the application to manage the policy.
private var policyService: PolicyService

/// The service used by the application to manage account state.
private let stateService: StateService

Expand Down Expand Up @@ -462,6 +465,7 @@ class DefaultAuthRepository {
/// - organizationService: The service used to manage syncing and updates to the user's organizations.
/// - organizationUserAPIService: The service used by the application to make organization
/// user-related API requests.
/// - policyService: The service used by the application to manage the policy.
/// - stateService: The service used by the application to manage account state.
/// - trustDeviceService: The service used by the application to manage trust device information.
/// - vaultTimeoutService: The service used by the application to manage vault access.
Expand All @@ -479,6 +483,7 @@ class DefaultAuthRepository {
organizationAPIService: OrganizationAPIService,
organizationService: OrganizationService,
organizationUserAPIService: OrganizationUserAPIService,
policyService: PolicyService,
stateService: StateService,
trustDeviceService: TrustDeviceService,
vaultTimeoutService: VaultTimeoutService
Expand All @@ -495,6 +500,7 @@ class DefaultAuthRepository {
self.organizationAPIService = organizationAPIService
self.organizationService = organizationService
self.organizationUserAPIService = organizationUserAPIService
self.policyService = policyService
self.stateService = stateService
self.trustDeviceService = trustDeviceService
self.vaultTimeoutService = vaultTimeoutService
Expand Down Expand Up @@ -736,6 +742,10 @@ extension DefaultAuthRepository: AuthRepository {
try await keychainService.deleteItems(for: userId)
await vaultTimeoutService.remove(userId: userId)

if await policyService.policyAppliesToUser(.removeUnlockWithPin) {
try await clearPins()
}

// Log the account out last.
try await stateService.logoutAccount(userId: userId, userInitiated: userInitiated)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
var keyConnectorService: MockKeyConnectorService!
var keychainService: MockKeychainRepository!
var organizationService: MockOrganizationService!
var policyService: MockPolicyService!
var subject: DefaultAuthRepository!
var stateService: MockStateService!
var vaultTimeoutService: MockVaultTimeoutService!
Expand Down Expand Up @@ -95,6 +96,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
keyConnectorService = MockKeyConnectorService()
keychainService = MockKeychainRepository()
organizationService = MockOrganizationService()
policyService = MockPolicyService()
stateService = MockStateService()
trustDeviceService = MockTrustDeviceService()
vaultTimeoutService = MockVaultTimeoutService()
Expand All @@ -112,6 +114,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
organizationAPIService: APIService(client: client),
organizationService: organizationService,
organizationUserAPIService: APIService(client: client),
policyService: policyService,
stateService: stateService,
trustDeviceService: trustDeviceService,
vaultTimeoutService: vaultTimeoutService
Expand All @@ -131,6 +134,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
errorReporter = nil
keychainService = nil
organizationService = nil
policyService = nil
subject = nil
8000 stateService = nil
vaultTimeoutService = nil
Expand Down Expand Up @@ -2045,6 +2049,8 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
vaultTimeoutService.isClientLocked[account.profile.userId] = false
biometricsRepository.capturedUserAuthKey = "Value"
biometricsRepository.setBiometricUnlockKeyError = nil
stateService.pinProtectedUserKeyValue["1"] = "1"
stateService.encryptedPinByUserId["1"] = "1"
let task = Task {
try await subject.logout(userInitiated: true)
}
Expand All @@ -2056,6 +2062,60 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
XCTAssertEqual(keychainService.deleteItemsForUserIds, ["1"])
XCTAssertTrue(stateService.logoutAccountUserInitiated)
XCTAssertEqual(vaultTimeoutService.removedIds, [anneAccount.profile.userId])
XCTAssertEqual(stateService.pinProtectedUserKeyValue["1"], "1")
XCTAssertEqual(stateService.encryptedPinByUserId["1"], "1")
}

/// `logout` successfully logs out a user clearing pins because of policy Remove unlock with pin being enabled.
func test_logout_successWhenClearingPins() {
let account = Account.fixture()
stateService.accounts = [account]
stateService.activeAccount = account
vaultTimeoutService.isClientLocked[account.profile.userId] = false
biometricsRepository.capturedUserAuthKey = "Value"
biometricsRepository.setBiometricUnlockKeyError = nil
stateService.pinProtectedUserKeyValue["1"] = "1"
stateService.encryptedPinByUserId["1"] = "1"
policyService.policyAppliesToUserResult[.removeUnlockWithPin] = true

let task = Task {
try await subject.logout(userInitiated: true)
}
waitFor(!vaultTimeoutService.removedIds.isEmpty)
task.cancel()

XCTAssertEqual([account.profile.userId], stateService.accountsLoggedOut)
XCTAssertNil(biometricsRepository.capturedUserAuthKey)
XCTAssertEqual(keychainService.deleteItemsForUserIds, ["1"])
XCTAssertTrue(stateService.logoutAccountUserInitiated)
XCTAssertEqual(vaultTimeoutService.removedIds, [anneAccount.profile.userId])
XCTAssertNil(stateService.pinProtectedUserKeyValue["1"])
XCTAssertNil(stateService.encryptedPinByUserId["1"])
}

/// `logout` throws when clearing pins and policy Remove unlock with pin being enabled.
func test_logout_throwsWhenClearingPins() async throws {
let account = Account.fixture()
stateService.accounts = [account]
stateService.activeAccount = nil
vaultTimeoutService.isClientLocked[account.profile.userId] = false
biometricsRepository.capturedUserAuthKey = "Value"
biometricsRepository.setBiometricUnlockKeyError = nil
stateService.pinProtectedUserKeyValue["1"] = "1"
stateService.encryptedPinByUserId["1"] = "1"
policyService.policyAppliesToUserResult[.removeUnlockWithPin] = true

await assertAsyncThrows(error: StateServiceError.noActiveAccount) {
try await subject.logout(userInitiated: true)
}

XCTAssertEqual([], stateService.accountsLoggedOut)
XCTAssertNotNil(biometricsRepository.capturedUserAuthKey)
XCTAssertEqual(keychainService.deleteItemsForUserIds, [])
XCTAssertFalse(stateService.logoutAccountUserInitiated)
XCTAssertEqual(vaultTimeoutService.removedIds, [])
XCTAssertEqual(stateService.pinProtectedUserKeyValue["1"], "1")
XCTAssertEqual(stateService.encryptedPinByUserId["1"], "1")
}

/// `unlockVault(password:)` throws an error if the vault is unable to be unlocked.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
organizationAPIService: apiService,
organizationService: organizationService,
organizationUserAPIService: apiService,
policyService: policyService,
stateService: stateService,
trustDeviceService: trustDeviceService,
vaultTimeoutService: vaultTimeoutService
Expand Down
3 changes: 3 additions & 0 deletions BitwardenShared/Core/Vault/Models/Enum/PolicyType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ enum PolicyType: Int, Codable {
/// Activates autofill with page load on the browser extension.
case activateAutofill = 11

/// If enabled, the setting to "Unlock with Pin" is hidden.
case removeUnlockWithPin = 14

/// An unknown policy type.
case unknown = -1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ final class AccountSecurityProcessor: StateProcessor<// swiftlint:disable:this t
/// Load any initial data for the view.
private func loadData() async {
do {
state.removeUnlockWithPinPolicyEnabled = await services.policyService.policyAppliesToUser(
.removeUnlockWithPin
)

state.biometricUnlockStatus = await loadBiometricUnlockPreference()

if try await services.authRepository.isPinUnlockAvailable() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class AccountSecurityProcessorTests: BitwardenTestCase { // swiftlint:disable:th
await subject.perform(.loadData)

XCTAssertTrue(subject.state.isUnlockWithPINCodeOn)
XCTAssertFalse(subject.state.removeUnlockWithPinPolicyEnabled)
}

/// `perform(_:)` with `.loadData` updates the state.
Expand Down Expand Up @@ -303,6 +304,19 @@ class AccountSecurityProcessorTests: BitwardenTestCase { // swiftlint:disable:th
XCTAssertEqual(stateService.accountSetupVaultUnlock["1"], .complete)
}

/// `perform(_:)` with `.loadData` loads the initial data for the view with remove unlock pin policy enabled.
@MainActor
func test_perform_loadData_removeUnlockPinPolicy() async {
stateService.activeAccount = .fixture()
authRepository.isPinUnlockAvailableResult = .success(true)
policyService.policyAppliesToUserResult[.removeUnlockWithPin] = true

await subject.perform(.loadData)

XCTAssertTrue(subject.state.isUnlockWithPINCodeOn)
XCTAssertTrue(subject.state.removeUnlockWithPinPolicyEnabled)
}

/// `perform(_:)` with `.lockVault` locks the user's vault.
@MainActor
func test_perform_lockVault() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ struct AccountSecurityState: Equatable {
/// The policy's timeout action, if set.
var policyTimeoutAction: SessionTimeoutAction?

/// Whether the policy to remove Unlock with pin feature is enabled.
var removeUnlockWithPinPolicyEnabled: Bool = false

/// The action taken when a session timeout occurs.
var sessionTimeoutAction: SessionTimeoutAction = .lock

Expand Down Expand Up @@ -295,4 +298,17 @@ struct AccountSecurityState: Equatable {
var policyTimeoutMinutes: Int {
policyTimeoutValue % 60
}

/// Whether to show/hide unlock options.
var showUnlockOptions: Bool {
guard case .available = biometricUnlockStatus else {
return unlockWithPinFeatureAvailable
}
return true
}

/// Whether the unlock with Pin feature is available.
var unlockWithPinFeatureAvailable: Bool {
!removeUnlockWithPinPolicyEnabled || isUnlockWithPINCodeOn
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ struct AccountSecurityView: View {

pendingLoginRequests

unlockOptionsSection
if store.state.showUnlockOptions {
unlockOptionsSection
}

authenticatorSyncSection

Expand Down Expand Up @@ -198,14 +200,16 @@ struct AccountSecurityView: View {
ContentBlock(dividerLeadingPadding: 16) {
biometricsSetting

BitwardenToggle(
Localizations.unlockWithPIN,
isOn: store.bindingAsync(
get: \.isUnlockWithPINCodeOn,
perform: AccountSecurityEffect.toggleUnlockWithPINCode
10000 if store.state.unlockWithPinFeatureAvailable {
BitwardenToggle(
Localizations.unlockWithPIN,
isOn: store.bindingAsync(
get: \.isUnlockWithPINCodeOn,
perform: AccountSecurityEffect.toggleUnlockWithPINCode
)
)
)
.accessibilityIdentifier("UnlockWithPinSwitch")
.accessibilityIdentifier("UnlockWithPinSwitch")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,34 @@ class AccountSecurityViewTests: BitwardenTestCase { // swiftlint:disable:this ty
XCTAssertEqual(processor.effects.last, .toggleUnlockWithPINCode(true))
}

/// When `.removeUnlockWithPin` policy is enabled and unlock with pin is disabled then Unlock with Pin is not shown.
@MainActor
func test_unlockWithPin_removeUnlockWithPinPolicyEnabled() throws {
processor.state.removeUnlockWithPinPolicyEnabled = true
XCTAssertThrowsError(try subject.inspect().find(toggleWithAccessibilityLabel: Localizations.unlockWithPIN))
}

/// When `.removeUnlockWithPin` policy is enabled and unlock with pin is enabled then Unlock with Pin is shown.
@MainActor
func test_unlockWithPin_removeUnlockWithPinPolicyEnabledWithPinEnabled() throws {
processor.state.removeUnlockWithPinPolicyEnabled = true
processor.state.isUnlockWithPINCodeOn = true
XCTAssertNoThrow(try subject.inspect().find(toggleWithAccessibilityLabel: Localizations.unlockWithPIN))
}

/// When `.removeUnlockWithPin` policy is enabled, unlock with pin disabled and biometrics is disabled then entire
/// Unlock options section is not shown.
@MainActor
func test_unlockWithPin_removeUnlockWithPinPolicyEnabledNoPinNorBiometrics() throws {
processor.state.removeUnlockWithPinPolicyEnabled = true
processor.state.isUnlockWithPINCodeOn = false
XCTAssertThrowsError(try subject.inspect().find(text: Localizations.unlockOptions))
XCTAssertThrowsError(try subject.inspect().find(toggleWithAccessibilityLabel: Localizations.unlockWithPIN))
XCTAssertThrowsError(try subject.inspect().find(ViewType.Toggle.self) { view in
try view.accessibilityIdentifier() == "UnlockWithBiometricsSwitch"
})
}

// MARK: Snapshots

/// The view renders correctly with the vault unlock action card is displayed.
Expand Down Expand Up @@ -266,6 +294,25 @@ class AccountSecurityViewTests: BitwardenTestCase { // swiftlint:disable:this ty
assertSnapshot(of: subject, as: .defaultPortrait)
}

/// The view renders correctly when the remove unlock with pin policy is enabled.
@MainActor
func test_snapshot_removeUnlockPinPolicyEnabled() {
let subject = AccountSecurityView(
store: Store(
processor: StateProcessor(
state: AccountSecurityState(
biometricUnlockStatus: .available(.faceID, enabled: true),
removeUnlockWithPinPolicyEnabled: true
)
)
)
)
assertSnapshots(
of: subject,
as: [.defaultPortrait, .defaultPortraitDark, .tallPortraitAX5()]
)
}

/// The view renders correctly when the `shouldShowAuthenticatorSyncSection` is `true`.
@MainActor
func test_snapshot_shouldShowAuthenticatorSyncSection() {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
0