Make sure enforce_element_pro is taken into account in FOSS (#7631)

* Make sure `enforce_element_pro` is taken into account in FOSS

* Make sure we crate `RustTemporaryMatrixClient` instances with a proper `serverName` value
This commit is contained in:
Jorge Martin Espinosa
2026-09-03 10:05:40 +02:00
committed by GitHub
parent 3b2e416855
commit dcd0bc782f
4 changed files with 92 additions and 14 deletions
@@ -10,6 +10,7 @@ import extension.testCommonDependencies
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.kotlin.serialization)
}
android {
@@ -22,8 +23,10 @@ dependencies {
implementation(projects.libraries.compound)
api(projects.features.enterprise.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.wellknown.api)
implementation(libs.serialization.json)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
@@ -14,18 +14,43 @@ import dev.zacsweers.metro.ContributesBinding
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.mapCatchingExceptions
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import timber.log.Timber
@ContributesBinding(AppScope::class)
class DefaultEnterpriseService : EnterpriseService {
class DefaultEnterpriseService(
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
private val jsonProvider: JsonProvider,
) : EnterpriseService {
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher) = url
override fun homeserverAllowList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override suspend fun isElementProEnforced(serverName: String): Boolean = false
override suspend fun isElementProEnforced(serverName: String): Boolean {
val temporaryMatrixClient = temporaryMatrixClientFactory.create(serverName).getOrElse { return false }
return temporaryMatrixClient.use { client ->
val baseUrl = serverName.ensureProtocol().removeSuffix("/")
// We'll always perform a network request here since we're not interested in any cached value.
client.getUrl("$baseUrl/.well-known/element/element.json")
.mapCatchingExceptions { response ->
val remoteConfig = jsonProvider().decodeFromString<MinimalEnterpriseConfig>(String(response))
remoteConfig.enforceElementPro ?: false
}
.onFailure {
Timber.e(it, "Failed to fetch enterprise config for checking if Element Pro is enforced for $serverName")
}
.getOrElse { false }
}
}
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
@@ -46,3 +71,11 @@ class DefaultEnterpriseService : EnterpriseService {
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
}
/**
* A minimal version of the enterprise config that only contains the fields we need to check if Element Pro is enforced.
*/
@Serializable
private data class MinimalEnterpriseConfig(
@SerialName("enforce_element_pro") val enforceElementPro: Boolean? = null,
)
@@ -12,33 +12,40 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClient
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.Test
class DefaultEnterpriseServiceTest {
@Test
fun homeserverWhitelist() {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.homeserverAllowList()).isEmpty()
}
@Test
fun `isAllowedToConnectToHomeserver is true for all homeserver urls`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.isAllowedToConnectToHomeserver(A_HOMESERVER_URL)).isTrue()
}
@Test
fun `isEnterpriseUser always return false`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.isEnterpriseUser(A_SESSION_ID)).isFalse()
}
@Test
fun `semanticColorsFlow always emits the same value`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
defaultEnterpriseService.semanticColorsFlow(null).test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
@@ -48,7 +55,7 @@ class DefaultEnterpriseServiceTest {
@Test
fun `brandColorsFlow always emits null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
defaultEnterpriseService.brandColorsFlow(null).test {
val initialState = awaitItem()
assertThat(initialState).isNull()
@@ -58,7 +65,7 @@ class DefaultEnterpriseServiceTest {
@Test
fun `semanticColorsFlow always emits the same value for a session`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
defaultEnterpriseService.semanticColorsFlow(A_SESSION_ID).test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
@@ -68,25 +75,25 @@ class DefaultEnterpriseServiceTest {
@Test
fun `overrideBrandColor has no effect`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "aColor")
}
@Test
fun `firebasePushGateway returns null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.firebasePushGateway()).isNull()
}
@Test
fun `unifiedPushDefaultPushGateway returns null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.unifiedPushDefaultPushGateway()).isNull()
}
@Test
fun `bugReportUrlFlow only emits UseDefault`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
defaultEnterpriseService.bugReportUrlFlow(A_SESSION_ID).test {
assertThat(awaitItem()).isEqualTo(BugReportUrl.UseDefault)
awaitComplete()
@@ -95,7 +102,34 @@ class DefaultEnterpriseServiceTest {
@Test
fun `getNoisyNotificationChannelId returns null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
val defaultEnterpriseService = createDefaultEnterpriseService()
assertThat(defaultEnterpriseService.getNoisyNotificationChannelId(A_SESSION_ID)).isNull()
}
@Test
fun `isElementProEnforced checks using a temporary client`() = runTest {
val closeLambda = lambdaRecorder<Unit> {}
val getUrlLambda = lambdaRecorder<String, Result<ByteArray>> {
Result.success("""{"enforce_element_pro": true}""".toByteArray())
}
val client = FakeTemporaryMatrixClient(
getUrlResult = getUrlLambda,
closeLambda = closeLambda,
)
val defaultEnterpriseService = createDefaultEnterpriseService(client = client)
assertThat(defaultEnterpriseService.isElementProEnforced(A_HOMESERVER_URL)).isTrue()
// Verify that the temporary client was used to fetch the URL and then closed
val expectedUrl = "${A_HOMESERVER_URL.ensureProtocol()}/.well-known/element/element.json"
getUrlLambda.assertions().isCalledOnce().with(value(expectedUrl))
closeLambda.assertions().isCalledOnce()
}
private fun createDefaultEnterpriseService(
client: FakeTemporaryMatrixClient = FakeTemporaryMatrixClient(),
jsonProvider: JsonProvider = { Json }
) = DefaultEnterpriseService(
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(createResult = { Result.success(client) }),
jsonProvider = jsonProvider,
)
}
@@ -10,9 +10,11 @@ package io.element.android.libraries.matrix.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.api.TemporaryMatrixClient
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import java.net.URL
@ContributesBinding(AppScope::class)
class RustTemporaryMatrixClientFactory(
@@ -21,6 +23,12 @@ class RustTemporaryMatrixClientFactory(
) : TemporaryMatrixClientFactory {
override suspend fun create(serverName: String): Result<TemporaryMatrixClient> {
return runCatchingExceptions {
// In case the 'serverName' is a full URL, we need to extract the host and port to pass to the client builder.
val parsedUrl = URL(serverName.ensureProtocol())
val domain = parsedUrl.host ?: error("Invalid server name: $serverName")
val port = parsedUrl.port
val formattedServerName = if (port != -1) "$domain:$port" else domain
val sessionPaths = sessionPathsFactory.create()
val client = rustMatrixClientFactory.getBaseClientBuilder(
sessionPaths = sessionPaths,
@@ -28,7 +36,7 @@ class RustTemporaryMatrixClientFactory(
slidingSyncType = ClientBuilderSlidingSync.Native,
isMessageSearchAvailable = false,
)
.serverName(serverName)
.serverName(formattedServerName)
.build()
RustTemporaryMatrixClient(client, sessionPaths)
}