Apply the GetUrlResolver pattern using unauthenticated matrix clients where possible

This commit is contained in:
Jorge Martín
2026-08-10 14:26:10 +02:00
committed by Jorge Martin Espinosa
parent 25eca29cd2
commit 90d4fbe0bd
8 changed files with 97 additions and 49 deletions
@@ -10,6 +10,7 @@ package io.element.android.features.enterprise.api
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.GetUrlResolver
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -17,7 +18,7 @@ import kotlinx.coroutines.flow.Flow
interface EnterpriseService { interface EnterpriseService {
val isEnterpriseBuild: Boolean val isEnterpriseBuild: Boolean
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
suspend fun tweakMasUrl(url: String, homeserver: String): String suspend fun tweakMasUrl(url: String, homeserver: String, getUrlResolver: GetUrlResolver): String
fun defaultHomeserverList(): List<String> fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
@@ -47,6 +48,11 @@ interface EnterpriseService {
*/ */
fun overriddenElementWellKnown(): ElementWellKnown? fun overriddenElementWellKnown(): ElementWellKnown?
/**
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
*/
fun essConfigEndpointUrl(domain: String): String?
companion object { companion object {
const val ANY_ACCOUNT_PROVIDER = "*" const val ANY_ACCOUNT_PROVIDER = "*"
} }
@@ -14,6 +14,7 @@ import dev.zacsweers.metro.ContributesBinding
import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.GetUrlResolver
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -24,7 +25,7 @@ class DefaultEnterpriseService : EnterpriseService {
override val isEnterpriseBuild = false override val isEnterpriseBuild = false
override suspend fun isEnterpriseUser(sessionId: SessionId) = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, homeserver: String) = url override suspend fun tweakMasUrl(url: String, homeserver: String, getUrlResolver: GetUrlResolver) = url
override fun defaultHomeserverList(): List<String> = emptyList() override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
@@ -48,4 +49,6 @@ class DefaultEnterpriseService : EnterpriseService {
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
override fun overriddenElementWellKnown(): ElementWellKnown? = null override fun overriddenElementWellKnown(): ElementWellKnown? = null
override fun essConfigEndpointUrl(domain: String): String? = null
} }
@@ -5,10 +5,12 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
package io.element.android.libraries.matrix.impl.scanner package io.element.android.features.enterprise.impl.scanner
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.GetUrlResolver
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
/** /**
@@ -16,5 +18,12 @@ import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
*/ */
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultContentScannerUrlProvider : ContentScannerUrlProvider { class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
override suspend fun getContentScannerUrl(homeserver: String): Result<String?> = Result.success(null) override suspend fun getContentScannerUrl(sessionId: SessionId): Result<String?> = Result.success(null)
@ContributesBinding(AppScope::class)
class Factory : ContentScannerUrlProvider.Factory {
override fun create(getUrlResolver: GetUrlResolver): ContentScannerUrlProvider {
return DefaultContentScannerUrlProvider()
}
}
} }
@@ -13,13 +13,15 @@ import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetriever
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultAccountProviderAccessControl( class DefaultAccountProviderAccessControl(
private val enterpriseService: EnterpriseService, private val enterpriseService: EnterpriseService,
private val wellknownRetriever: WellknownRetriever, private val wellknownRetrieverFactory: WellknownRetriever.Factory,
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
) : AccountProviderAccessControl { ) : AccountProviderAccessControl {
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try { override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
assertIsAllowedToConnectToAccountProvider( assertIsAllowedToConnectToAccountProvider(
@@ -38,14 +40,21 @@ class DefaultAccountProviderAccessControl(
) { ) {
if (enterpriseService.isEnterpriseBuild.not()) { if (enterpriseService.isEnterpriseBuild.not()) {
// Ensure that Element Pro is not required for this account provider // Ensure that Element Pro is not required for this account provider
val wellKnown = wellknownRetriever.getElementWellKnown( val temporaryMatrixClient = temporaryMatrixClientFactory.createTemporaryMatrixClient(accountProviderUrl).getOrThrow()
baseUrl = accountProviderUrl.ensureProtocol(), temporaryMatrixClient.use {
).dataOrNull() val wellknownRetriever = wellknownRetrieverFactory.create(temporaryMatrixClient)
if (wellKnown?.enforceElementPro == true) { val wellKnown = wellknownRetriever.getElementWellKnown(
throw AccountProviderAccessException.NeedElementProException( host = accountProviderUrl,
unauthorisedAccountProviderTitle = title, // It's expected this is hardcoded to the well-known endpoint
applicationId = ELEMENT_PRO_APPLICATION_ID, source = EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT,
) ).dataOrNull()
if (wellKnown?.enforceElementPro == true) {
throw AccountProviderAccessException.NeedElementProException(
unauthorisedAccountProviderTitle = title,
applicationId = ELEMENT_PRO_APPLICATION_ID,
)
}
} }
} }
if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) { if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
@@ -13,6 +13,8 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.appconfig.AuthenticationConfig import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetriever
import timber.log.Timber import timber.log.Timber
@@ -22,24 +24,32 @@ interface WebClientUrlForAuthenticationRetriever {
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultWebClientUrlForAuthenticationRetriever( class DefaultWebClientUrlForAuthenticationRetriever(
private val wellknownRetriever: WellknownRetriever, private val wellknownRetrieverFactory: WellknownRetriever.Factory,
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
) : WebClientUrlForAuthenticationRetriever { ) : WebClientUrlForAuthenticationRetriever {
override suspend fun retrieve(homeServerUrl: String): String { override suspend fun retrieve(homeServerUrl: String): String {
if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) { if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) {
Timber.w("Temporary account creation flow is only supported on matrix.org") Timber.w("Temporary account creation flow is only supported on matrix.org")
throw AccountCreationNotSupported() throw AccountCreationNotSupported()
} }
val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl).dataOrNull() val temporaryMatrixClient = temporaryMatrixClientFactory.createTemporaryMatrixClient(homeServerUrl).getOrThrow()
?: throw AccountCreationNotSupported() return temporaryMatrixClient.use {
val registrationHelperUrl = wellknown.registrationHelperUrl val wellknownRetriever = wellknownRetrieverFactory.create(temporaryMatrixClient)
return if (registrationHelperUrl != null) { val wellknown = wellknownRetriever.getElementWellKnown(
registrationHelperUrl.toUri() host = homeServerUrl,
.buildUpon() source = EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT
.appendQueryParameter("hs_url", homeServerUrl) ).dataOrNull() ?: throw AccountCreationNotSupported()
.build()
.toString() val registrationHelperUrl = wellknown.registrationHelperUrl
} else { if (registrationHelperUrl != null) {
throw AccountCreationNotSupported() registrationHelperUrl.toUri()
.buildUpon()
.appendQueryParameter("hs_url", homeServerUrl)
.build()
.toString()
} else {
throw AccountCreationNotSupported()
}
} }
} }
} }
@@ -7,12 +7,19 @@
package io.element.android.libraries.matrix.api.scanner package io.element.android.libraries.matrix.api.scanner
import io.element.android.libraries.matrix.api.GetUrlResolver
import io.element.android.libraries.matrix.api.core.SessionId
/** /**
* Provides the URL of the content scanner service for a given homeserver, if any is set up. * Provides the URL of the content scanner service for a given homeserver, if any is set up.
*/ */
fun interface ContentScannerUrlProvider { fun interface ContentScannerUrlProvider {
/** /**
* Returns the URL of the content scanner service for the given [homeserver], or `null` if no content scanner is set up. * Returns the URL of the content scanner service for the given [sessionId], or `null` if no content scanner is set up.
*/ */
suspend fun getContentScannerUrl(homeserver: String): Result<String?> suspend fun getContentScannerUrl(sessionId: SessionId): Result<String?>
fun interface Factory {
fun create(getUrlResolver: GetUrlResolver): ContentScannerUrlProvider
}
} }
@@ -17,7 +17,7 @@ import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
import io.element.android.libraries.matrix.impl.analytics.UtdTracker import io.element.android.libraries.matrix.impl.analytics.UtdTracker
@@ -70,7 +70,7 @@ class RustMatrixClientFactory(
private val clientBuilderProvider: ClientBuilderProvider, private val clientBuilderProvider: ClientBuilderProvider,
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider, private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
private val workManagerScheduler: WorkManagerScheduler, private val workManagerScheduler: WorkManagerScheduler,
private val contentScannerUrlProvider: ContentScannerUrlProvider, private val contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory,
) { ) {
private val sessionDelegate = RustClientSessionDelegate( private val sessionDelegate = RustClientSessionDelegate(
sessionStore = sessionStore, sessionStore = sessionStore,
@@ -135,21 +135,19 @@ class RustMatrixClientFactory(
client.setUtdDelegate(UtdTracker(analyticsService)) client.setUtdDelegate(UtdTracker(analyticsService))
val domainName = UserId(client.userId()).domainName
// If a content scanner URL is available for the homeserver, create a RustContentScanner and set it on the client. // If a content scanner URL is available for the homeserver, create a RustContentScanner and set it on the client.
// This allows the SDK to use the content scanner for automatic media scanning. // This allows the SDK to use the content scanner for automatic media scanning.
// If no content scanner URL is available, the contentScanner will be null. // If no content scanner URL is available, the contentScanner will be null.
val contentScanner = domainName?.let { val contentScannerUrlProvider = contentScannerUrlProviderFactory.create(RustTemporaryMatrixClient(client, null))
contentScannerUrlProvider.getContentScannerUrl(domainName) val contentScanner = contentScannerUrlProvider.getContentScannerUrl(SessionId(client.userId()))
.getOrNull() .getOrNull()
?.let { contentScannerUrl -> ?.let { contentScannerUrl ->
val contentScanner = ContentScanner(contentScannerUrl) val contentScanner = ContentScanner(contentScannerUrl)
client.setContentScanner(contentScanner) client.setContentScanner(contentScanner)
RustContentScanner( RustContentScanner(
client = client, client = client,
rustScanner = contentScanner, rustScanner = contentScanner,
) )
}
} }
val syncService = client.syncService() val syncService = client.syncService()
@@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.paths.SessionPaths
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.RustTemporaryMatrixClient
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
@@ -133,9 +134,10 @@ class RustMatrixAuthenticationService(
} }
currentClient = client currentClient = client
client.homeserverLoginDetails().map() client.homeserverLoginDetails().map()
}.onFailure { }.onFailure {
clear() clear(destroyClient = true)
}.mapFailure { failure -> }.mapFailure { failure ->
Timber.e(failure, "Failed to set homeserver to $homeserver") Timber.e(failure, "Failed to set homeserver to $homeserver")
failure.mapAuthenticationException() failure.mapAuthenticationException()
@@ -168,7 +170,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData) sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary // Clean up the strong reference held here since it's no longer necessary
currentClient = null clear(destroyClient = false)
SessionId(sessionData.userId) SessionId(sessionData.userId)
}.mapFailure { failure -> }.mapFailure { failure ->
@@ -248,7 +250,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData) sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary // Clean up the strong reference held here since it's no longer necessary
currentClient = null clear(destroyClient = false)
SessionId(sessionData.userId) SessionId(sessionData.userId)
} }
@@ -271,11 +273,13 @@ class RustMatrixAuthenticationService(
deviceId = null, deviceId = null,
additionalScopes = emptyList(), additionalScopes = emptyList(),
) )
val getUrlResolver = RustTemporaryMatrixClient(client, sessionPaths)
val url = oAuthAuthorizationData.loginUrl() val url = oAuthAuthorizationData.loginUrl()
.let { .let {
enterpriseService.tweakMasUrl( enterpriseService.tweakMasUrl(
url = it, url = it,
homeserver = client.server() ?: client.homeserver(), homeserver = client.server() ?: client.homeserver(),
getUrlResolver = getUrlResolver,
) )
} }
pendingOAuthAuthorizationData = oAuthAuthorizationData pendingOAuthAuthorizationData = oAuthAuthorizationData
@@ -335,7 +339,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData) sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary // Clean up the strong reference held here since it's no longer necessary
currentClient = null clear(destroyClient = false)
SessionId(sessionData.userId) SessionId(sessionData.userId)
}.mapFailure { failure -> }.mapFailure { failure ->
@@ -398,7 +402,7 @@ class RustMatrixAuthenticationService(
sessionStore.addSession(sessionData) sessionStore.addSession(sessionData)
// Clean up the strong reference held here since it's no longer necessary // Clean up the strong reference held here since it's no longer necessary
currentClient = null clear(destroyClient = false)
SessionId(sessionData.userId) SessionId(sessionData.userId)
}.mapFailure { }.mapFailure {
@@ -464,8 +468,10 @@ class RustMatrixAuthenticationService(
.build() .build()
} }
private fun clear() { private fun clear(destroyClient: Boolean) {
currentClient?.close() if (destroyClient) {
currentClient?.close()
}
currentClient = null currentClient = null
} }