Add local network address classifier and permission advisor

This commit is contained in:
ganfra
2026-07-07 21:37:32 +02:00
parent 7c3d1e2e80
commit 74bc2c0f5d
9 changed files with 405 additions and 0 deletions
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl
import android.Manifest.permission.ACCESS_LOCAL_NETWORK
import android.os.Build
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier
import io.element.android.libraries.androidutils.network.LocalNetworkClassification
import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ContributesBinding(AppScope::class)
class DefaultLocalNetworkPermissionAdvisor(
private val classifier: LocalNetworkAddressClassifier,
private val permissionStateProvider: PermissionStateProvider,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) : LocalNetworkPermissionAdvisor {
override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean {
if (!buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.CINNAMON_BUN)) return false
if (permissionStateProvider.isPermissionGranted(ACCESS_LOCAL_NETWORK)) return false
return when (classifier.classify(homeserverUrl)) {
LocalNetworkClassification.LocalIp -> true
LocalNetworkClassification.PublicIp,
// Unresolvable: we can't confirm the host is local, so we don't prompt, acceptable trade-off vs. over-prompting.
LocalNetworkClassification.Unresolvable -> false
}
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.permissions.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier
import io.element.android.libraries.androidutils.network.LocalNetworkClassification
import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import kotlinx.coroutines.test.runTest
import org.junit.Test
private const val LOCAL_URL = "https://matrix.corp.internal"
private const val PUBLIC_URL = "https://matrix.org"
class DefaultLocalNetworkPermissionAdvisorTest {
private fun advisor(
sdkInt: Int,
permissionGranted: Boolean,
classification: LocalNetworkClassification,
) = DefaultLocalNetworkPermissionAdvisor(
classifier = FakeLocalNetworkAddressClassifier(classification),
permissionStateProvider = FakePermissionStateProvider(permissionGranted = permissionGranted),
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkInt),
)
@Test
fun `returns false when SDK below 37`() = runTest {
val result = advisor(sdkInt = 36, permissionGranted = false, classification = LocalNetworkClassification.LocalIp)
.shouldRequestPermissionFor(LOCAL_URL)
assertThat(result).isFalse()
}
@Test
fun `returns false when permission already granted`() = runTest {
val result = advisor(sdkInt = 37, permissionGranted = true, classification = LocalNetworkClassification.LocalIp)
.shouldRequestPermissionFor(LOCAL_URL)
assertThat(result).isFalse()
}
@Test
fun `returns false when URL classified as public IP`() = runTest {
val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.PublicIp)
.shouldRequestPermissionFor(PUBLIC_URL)
assertThat(result).isFalse()
}
@Test
fun `returns true when SDK 37, permission missing, URL is local`() = runTest {
val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.LocalIp)
.shouldRequestPermissionFor(LOCAL_URL)
assertThat(result).isTrue()
}
@Test
fun `returns false when SDK 37, permission missing, URL unresolvable`() = runTest {
val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.Unresolvable)
.shouldRequestPermissionFor(LOCAL_URL)
assertThat(result).isFalse()
}
@Test
fun `returns true when SDK above 37, permission missing, URL is local`() = runTest {
val result = advisor(sdkInt = 38, permissionGranted = false, classification = LocalNetworkClassification.LocalIp)
.shouldRequestPermissionFor(LOCAL_URL)
assertThat(result).isTrue()
}
}
private class FakeLocalNetworkAddressClassifier(
private val classification: LocalNetworkClassification,
) : LocalNetworkAddressClassifier {
override suspend fun classify(url: String): LocalNetworkClassification = classification
}