mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-2411: Add logic for managed device pre-configured URLs (#3358)
This commit is contained in:
parent
63e7465433
commit
d9b1809e58
8 changed files with 411 additions and 0 deletions
|
@ -150,6 +150,11 @@
|
|||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
|||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -28,4 +29,7 @@ class BitwardenApplication : Application() {
|
|||
|
||||
@Inject
|
||||
lateinit var organizationEventManager: OrganizationEventManager
|
||||
|
||||
@Inject
|
||||
lateinit var restrictionManager: RestrictionManager
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager.di
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
|
@ -41,6 +42,8 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
|||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
|
||||
|
@ -211,4 +214,19 @@ object PlatformManagerModule {
|
|||
context = context,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRestrictionManager(
|
||||
@ApplicationContext context: Context,
|
||||
appForegroundManager: AppForegroundManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
): RestrictionManager = RestrictionManagerImpl(
|
||||
appForegroundManager = appForegroundManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
context = context,
|
||||
environmentRepository = environmentRepository,
|
||||
restrictionsManager = requireNotNull(context.getSystemService()),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager.restriction
|
||||
|
||||
/**
|
||||
* A manager for handling restrictions.
|
||||
*/
|
||||
interface RestrictionManager
|
|
@ -0,0 +1,123 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager.restriction
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.RestrictionsManager
|
||||
import android.os.Bundle
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* The default implementation of the [RestrictionManager].
|
||||
*/
|
||||
class RestrictionManagerImpl(
|
||||
appForegroundManager: AppForegroundManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val context: Context,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val restrictionsManager: RestrictionsManager,
|
||||
) : RestrictionManager {
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
private val intentFilter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
|
||||
private val restrictionsChangedReceiver = RestrictionsChangedReceiver()
|
||||
private var isReceiverRegistered = false
|
||||
|
||||
init {
|
||||
appForegroundManager
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
when (it) {
|
||||
AppForegroundState.BACKGROUNDED -> handleBackground()
|
||||
AppForegroundState.FOREGROUNDED -> handleForeground()
|
||||
}
|
||||
}
|
||||
.launchIn(mainScope)
|
||||
}
|
||||
|
||||
private fun handleBackground() {
|
||||
if (isReceiverRegistered) {
|
||||
context.unregisterReceiver(restrictionsChangedReceiver)
|
||||
}
|
||||
isReceiverRegistered = false
|
||||
}
|
||||
|
||||
private fun handleForeground() {
|
||||
context.registerReceiver(restrictionsChangedReceiver, intentFilter)
|
||||
isReceiverRegistered = true
|
||||
updatePreconfiguredRestrictionSettings()
|
||||
}
|
||||
|
||||
private fun updatePreconfiguredRestrictionSettings() {
|
||||
restrictionsManager
|
||||
.applicationRestrictions
|
||||
?.takeUnless { it.isEmpty }
|
||||
?.let { setPreconfiguredSettings(it) }
|
||||
}
|
||||
|
||||
private fun setPreconfiguredSettings(bundle: Bundle) {
|
||||
bundle
|
||||
.getString(BASE_ENVIRONMENT_URL_RESTRICTION_KEY)
|
||||
?.let { url -> setPreconfiguredUrl(baseEnvironmentUrl = url) }
|
||||
}
|
||||
|
||||
private fun setPreconfiguredUrl(baseEnvironmentUrl: String) {
|
||||
environmentRepository.environment = when (val current = environmentRepository.environment) {
|
||||
Environment.Us -> {
|
||||
when (baseEnvironmentUrl) {
|
||||
// If the base matches the predefined US environment, leave it alone
|
||||
Environment.Us.environmentUrlData.base -> current
|
||||
// If the base does not match the predefined US environment, create a
|
||||
// self-hosted environment with the new base
|
||||
else -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
Environment.Eu -> {
|
||||
when (baseEnvironmentUrl) {
|
||||
// If the base matches the predefined EU environment, leave it alone
|
||||
Environment.Eu.environmentUrlData.base -> current
|
||||
// If the base does not match the predefined EU environment, create a
|
||||
// self-hosted environment with the new base
|
||||
else -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
is Environment.SelfHosted -> current.toSelfHosted(base = baseEnvironmentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [BroadcastReceiver] used to listen for [Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED]
|
||||
* updates.
|
||||
*
|
||||
* Note: The `Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED` will only be received if the
|
||||
* `BroadcastReceiver` is dynamically registered, so this cannot be registered in the manifest.
|
||||
*/
|
||||
private inner class RestrictionsChangedReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) {
|
||||
updatePreconfiguredRestrictionSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val BASE_ENVIRONMENT_URL_RESTRICTION_KEY: String = "baseEnvironmentUrl"
|
||||
|
||||
/**
|
||||
* Helper method for creating a new [Environment.SelfHosted] with a new base.
|
||||
*/
|
||||
private fun Environment.toSelfHosted(
|
||||
base: String,
|
||||
): Environment.SelfHosted =
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = environmentUrlData.copy(base = base),
|
||||
)
|
|
@ -18,4 +18,5 @@
|
|||
<string name="continue_to_complete_web_authn_verfication" translatable="false">Continue to complete WebAuthn verification.</string>
|
||||
<string name="launch_web_authn" translatable="false">Launch WebAuthn</string>
|
||||
<string name="there_was_an_error_starting_web_authn_two_factor_authentication" translatable="false">There was an error starting WebAuthn two factor authentication</string>
|
||||
<string name="self_hosted_server_url" translatable="false">Self-hosted server URL</string>
|
||||
</resources>
|
||||
|
|
9
app/src/main/res/xml/app_restrictions.xml
Normal file
9
app/src/main/res/xml/app_restrictions.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<restriction
|
||||
android:defaultValue="@null"
|
||||
android:description="@string/server_url"
|
||||
android:key="baseEnvironmentUrl"
|
||||
android:restrictionType="string"
|
||||
android:title="@string/self_hosted_server_url" />
|
||||
</restrictions>
|
|
@ -0,0 +1,245 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager.restriction
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.RestrictionsManager
|
||||
import android.os.Bundle
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.FakeAppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
class RestrictionManagerTest {
|
||||
|
||||
private val context = mockk<Context> {
|
||||
every { registerReceiver(any(), any()) } returns null
|
||||
every { unregisterReceiver(any()) } just runs
|
||||
}
|
||||
private val fakeAppForegroundManager = FakeAppForegroundManager()
|
||||
private val fakeDispatcherManager = FakeDispatcherManager().apply {
|
||||
setMain(unconfined)
|
||||
}
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
private val restrictionsManager = mockk<RestrictionsManager>()
|
||||
|
||||
private val restrictionManager: RestrictionManager = RestrictionManagerImpl(
|
||||
appForegroundManager = fakeAppForegroundManager,
|
||||
dispatcherManager = fakeDispatcherManager,
|
||||
context = context,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
restrictionsManager = restrictionsManager,
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
fakeDispatcherManager.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on app foreground with a null bundle should register receiver and do nothing else`() {
|
||||
every { restrictionsManager.applicationRestrictions } returns null
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on app foreground with an empty bundle should register receiver and do nothing else`() {
|
||||
every { restrictionsManager.applicationRestrictions } returns mockBundle()
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on app foreground with unknown bundle data should register receiver and do nothing else`() {
|
||||
every {
|
||||
restrictionsManager.applicationRestrictions
|
||||
} returns mockBundle("key" to "unknown")
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on app foreground with baseEnvironmentUrl bundle data matching the current US environment should register receiver and set the environment to US`() {
|
||||
every {
|
||||
restrictionsManager.applicationRestrictions
|
||||
} returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.com")
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(Environment.Us, fakeEnvironmentRepository.environment)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on app foreground with baseEnvironmentUrl bundle data not matching the current US environment should register receiver and set the environment to self-hosted`() {
|
||||
val baseUrl = "https://other.bitwarden.com"
|
||||
every {
|
||||
restrictionsManager.applicationRestrictions
|
||||
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = Environment.Us.environmentUrlData.copy(base = baseUrl),
|
||||
),
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on app foreground with baseEnvironmentUrl bundle data matching the current EU environment should register receiver and set the environment to EU`() {
|
||||
fakeEnvironmentRepository.environment = Environment.Eu
|
||||
every {
|
||||
restrictionsManager.applicationRestrictions
|
||||
} returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.eu")
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(Environment.Eu, fakeEnvironmentRepository.environment)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on app foreground with baseEnvironmentUrl bundle data not matching the current EU environment should register receiver and set the environment to self-hosted`() {
|
||||
val baseUrl = "https://other.bitwarden.eu"
|
||||
fakeEnvironmentRepository.environment = Environment.Eu
|
||||
every {
|
||||
restrictionsManager.applicationRestrictions
|
||||
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = Environment.Eu.environmentUrlData.copy(base = baseUrl),
|
||||
),
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on app foreground with baseEnvironmentUrl bundle data matching the current self-hosted environment should register receiver and set the environment to self-hosted`() {
|
||||
val baseUrl = "https://vault.qa.bitwarden.pw"
|
||||
val environment = Environment.SelfHosted(
|
||||
environmentUrlData = EnvironmentUrlDataJson(base = baseUrl),
|
||||
)
|
||||
fakeEnvironmentRepository.environment = environment
|
||||
every {
|
||||
restrictionsManager.applicationRestrictions
|
||||
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(environment, fakeEnvironmentRepository.environment)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on app foreground with baseEnvironmentUrl bundle data not matching the current self-hosted environment should register receiver and set the environment to self-hosted`() {
|
||||
val baseUrl = "https://other.qa.bitwarden.pw"
|
||||
val environment = Environment.SelfHosted(
|
||||
environmentUrlData = EnvironmentUrlDataJson(base = "https://vault.qa.bitwarden.pw"),
|
||||
)
|
||||
fakeEnvironmentRepository.environment = environment
|
||||
every {
|
||||
restrictionsManager.applicationRestrictions
|
||||
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.registerReceiver(any(), any())
|
||||
}
|
||||
assertEquals(
|
||||
Environment.SelfHosted(
|
||||
environmentUrlData = environment.environmentUrlData.copy(base = baseUrl),
|
||||
),
|
||||
fakeEnvironmentRepository.environment,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on app background when not foregrounded should do nothing`() {
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
|
||||
|
||||
verify(exactly = 0) {
|
||||
context.unregisterReceiver(any())
|
||||
restrictionsManager.applicationRestrictions
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on app background after foreground should unregister receiver`() {
|
||||
every { restrictionsManager.applicationRestrictions } returns null
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
|
||||
clearMocks(context, restrictionsManager, answers = false)
|
||||
|
||||
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
|
||||
|
||||
verify(exactly = 1) {
|
||||
context.unregisterReceiver(any())
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
restrictionsManager.applicationRestrictions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for constructing a simple mock bundle.
|
||||
*/
|
||||
private fun mockBundle(vararg pairs: Pair<String, String>): Bundle =
|
||||
mockk<Bundle> {
|
||||
every { isEmpty } returns pairs.isEmpty()
|
||||
every { getString(any()) } returns null
|
||||
pairs.forEach { (key, value) ->
|
||||
every { getString(key) } returns value
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue