Skip to content
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

Feature/fga/authenticated media #8868

Merged
merged 8 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ interface Session {
*/
fun getOkHttpClient(): OkHttpClient

/**
* Same as [getOkHttpClient] but will add the access token to the request.
*/
fun getAuthenticatedOkHttpClient(): OkHttpClient

/**
* A global session listener to get notified for some events.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ interface ContentUrlResolver {
*/
fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String?

fun requiresAuthentication(resolvedUrl: String): Boolean

sealed class ResolvedMethod {
data class GET(val url: String) : ResolvedMethod()
data class POST(val url: String, val jsonBody: String) : ResolvedMethod()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ data class HomeServerCapabilities(
* If set to true, the SDK will not use the network constraint when configuring Worker for the WorkManager, provided in Wellknown.
*/
val disableNetworkConstraint: Boolean? = null,
/**
* True if the home server supports authenticated media.
*/
val canUseAuthenticatedMedia: Boolean = false,
) {

enum class RoomCapabilitySupport {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentScannerService
import org.matrix.android.sdk.internal.session.media.IsAuthenticatedMediaSupported

internal class DefaultLoginWizard(
private val authAPI: AuthAPI,
Expand All @@ -45,8 +46,14 @@ internal class DefaultLoginWizard(
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")

private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
authAPI,
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService())
authAPI = authAPI,
contentUrlResolver = DefaultContentUrlResolver(
homeServerConnectionConfig = pendingSessionData.homeServerConnectionConfig,
scannerService = DisabledContentScannerService(),
isAuthenticatedMediaSupported = object : IsAuthenticatedMediaSupported {
override fun invoke() = false
}
)
)

override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ internal data class HomeServerVersion(
val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
val v1_4_0 = HomeServerVersion(major = 1, minor = 4, patch = 0)
val v1_11_0 = HomeServerVersion(major = 1, minor = 11, patch = 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"

@Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow")
private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882"
private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771"
Expand Down Expand Up @@ -142,6 +143,15 @@ internal fun Versions.doesServerSupportLogoutDevices(): Boolean {
return getMaxVersion() >= HomeServerVersion.r0_6_1
}

/**
* Indicate if the server supports MSC3916 : https://github.com/matrix-org/matrix-spec-proposals/pull/3916
*
* @return true if authenticated media is supported
*/
internal fun Versions.doesServerSupportAuthenticatedMedia(): Boolean {
return getMaxVersion() >= HomeServerVersion.v1_11_0
}

private fun Versions.getMaxVersion(): HomeServerVersion {
return supportedVersions
?.mapNotNull { HomeServerVersion.parse(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo053
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo054
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo055
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo056
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
Expand All @@ -80,7 +81,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 55L,
schemaVersion = 56L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
Expand Down Expand Up @@ -145,5 +146,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 53) MigrateSessionTo053(realm).perform()
if (oldVersion < 54) MigrateSessionTo054(realm).perform()
if (oldVersion < 55) MigrateSessionTo055(realm).perform()
if (oldVersion < 56) MigrateSessionTo056(realm).perform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ internal object HomeServerCapabilitiesMapper {
externalAccountManagementUrl = entity.externalAccountManagementUrl,
authenticationIssuer = entity.authenticationIssuer,
disableNetworkConstraint = entity.disableNetworkConstraint,
canUseAuthenticatedMedia = entity.canUseAuthenticatedMedia,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.matrix.android.sdk.internal.database.migration

import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
import org.matrix.android.sdk.internal.util.database.RealmMigrator

internal class MigrateSessionTo056(realm: DynamicRealm) : RealmMigrator(realm, 56) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_AUTHENTICATED_MEDIA, Boolean::class.java)
?.forceRefreshOfHomeServerCapabilities()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal open class HomeServerCapabilitiesEntity(
var externalAccountManagementUrl: String? = null,
var authenticationIssuer: String? = null,
var disableNetworkConstraint: Boolean? = null,
var canUseAuthenticatedMedia: Boolean = false,
) : RealmObject() {

companion object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.network

import okhttp3.Interceptor
import okhttp3.Response
import org.matrix.android.sdk.internal.network.httpclient.addAuthenticationHeader
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider

internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor {
Expand All @@ -28,7 +29,7 @@ internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTok
// Add the access token to all requests if it is set
accessTokenProvider.getToken()?.let { token ->
val newRequestBuilder = request.newBuilder()
newRequestBuilder.header(HttpHeaders.Authorization, "Bearer $token")
newRequestBuilder.addAuthenticationHeader(token)
request = newRequestBuilder.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
package org.matrix.android.sdk.internal.network.httpclient

import okhttp3.OkHttpClient
import okhttp3.Request
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.internal.network.AccessTokenInterceptor
import org.matrix.android.sdk.internal.network.HttpHeaders
import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor
import org.matrix.android.sdk.internal.network.ssl.CertUtil
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
Expand Down Expand Up @@ -66,3 +68,10 @@ internal fun OkHttpClient.Builder.applyMatrixConfiguration(matrixConfiguration:

return this
}

fun Request.Builder.addAuthenticationHeader(accessToken: String?): Request.Builder {
if (accessToken != null) {
header(HttpHeaders.Authorization, "Bearer $accessToken")
}
return this
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.util.md5
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
import org.matrix.android.sdk.internal.network.httpclient.addAuthenticationHeader
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
import org.matrix.android.sdk.internal.util.file.AtomicFileCreator
import org.matrix.android.sdk.internal.util.time.Clock
Expand All @@ -54,6 +57,7 @@ internal class DefaultFileService @Inject constructor(
private val okHttpClient: OkHttpClient,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock,
@Authenticated private val accessTokenProvider: AccessTokenProvider,
) : FileService {

// Legacy folder, will be deleted
Expand Down Expand Up @@ -124,21 +128,26 @@ internal class DefaultFileService @Inject constructor(
val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)

if (!cachedFiles.file.exists()) {
val resolvedUrl = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null")
val resolvedMethod = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null")

val request = when (resolvedUrl) {
val request = when (resolvedMethod) {
is ContentUrlResolver.ResolvedMethod.GET -> {
Request.Builder()
.url(resolvedUrl.url)
val requestBuilder = Request.Builder()
.url(resolvedMethod.url)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.build()

if (contentUrlResolver.requiresAuthentication(resolvedMethod.url)) {
val accessToken = accessTokenProvider.getToken()
requestBuilder.addAuthenticationHeader(accessToken)
}
requestBuilder.build()
}

is ContentUrlResolver.ResolvedMethod.POST -> {
Request.Builder()
.url(resolvedUrl.url)
.url(resolvedMethod.url)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.post(resolvedUrl.jsonBody.toRequestBody("application/json".toMediaType()))
.post(resolvedMethod.jsonBody.toRequestBody("application/json".toMediaType()))
.build()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import org.matrix.android.sdk.api.util.appendParamToUrl
import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH
import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.di.ContentScannerDatabase
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.IdentityDatabase
Expand Down Expand Up @@ -131,6 +132,8 @@ internal class DefaultSession @Inject constructor(
private val eventStreamService: Lazy<EventStreamService>,
@UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
@Authenticated
private val authenticatedOkHttpClient: Lazy<OkHttpClient>,
private val sessionState: SessionState,
) : Session,
GlobalErrorHandler.Listener {
Expand Down Expand Up @@ -234,6 +237,10 @@ internal class DefaultSession @Inject constructor(
return unauthenticatedWithCertificateOkHttpClient.get()
}

override fun getAuthenticatedOkHttpClient(): OkHttpClient {
return authenticatedOkHttpClient.get()
}

override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import org.matrix.android.sdk.internal.session.events.DefaultEventService
import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager
import org.matrix.android.sdk.internal.session.media.DefaultIsAuthenticatedMediaSupported
import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
Expand Down Expand Up @@ -365,6 +366,10 @@ internal abstract class SessionModule {
@IntoSet
abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver

@Binds
@IntoSet
abstract fun bindIsMediaAuthenticated(observer: DefaultIsAuthenticatedMediaSupported): SessionLifecycleObserver

@Binds
@IntoSet
abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@ import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
import org.matrix.android.sdk.internal.session.contentscanner.model.toJson
import org.matrix.android.sdk.internal.session.media.IsAuthenticatedMediaSupported
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
import javax.inject.Inject

internal class DefaultContentUrlResolver @Inject constructor(
homeServerConnectionConfig: HomeServerConnectionConfig,
private val scannerService: ContentScannerService
private val scannerService: ContentScannerService,
private val isAuthenticatedMediaSupported: IsAuthenticatedMediaSupported,
) : ContentUrlResolver {

private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()

private val authenticatedMediaApiPath = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_V1 + "media/"
override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"

override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? {
Expand Down Expand Up @@ -80,15 +82,20 @@ internal class DefaultContentUrlResolver @Inject constructor(
}
}

override fun requiresAuthentication(resolvedUrl: String): Boolean {
return resolvedUrl.startsWith(authenticatedMediaApiPath)
}

private fun resolve(
contentUrl: String,
toThumbnail: Boolean,
params: String = ""
): String {
var serverAndMediaId = contentUrl.removeMxcPrefix()

val apiPath = if (scannerService.isScannerEnabled()) {
NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE
} else if (isAuthenticatedMediaSupported()) {
NetworkConstants.URI_API_PREFIX_PATH_V1 + "media/"
} else {
NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.auth.version.Versions
import org.matrix.android.sdk.internal.auth.version.doesServerSupportAuthenticatedMedia
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin
import org.matrix.android.sdk.internal.auth.version.doesServerSupportRedactionOfRelatedEvents
Expand All @@ -39,7 +40,7 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor
import org.matrix.android.sdk.internal.session.media.GetMediaConfigResult
import org.matrix.android.sdk.internal.session.media.MediaAPI
import org.matrix.android.sdk.internal.session.media.MediaAPIProvider
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
Expand All @@ -55,7 +56,7 @@ internal interface GetHomeServerCapabilitiesTask : Task<GetHomeServerCapabilitie

internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val capabilitiesAPI: CapabilitiesAPI,
private val mediaAPI: MediaAPI,
private val mediaAPIProvider: MediaAPIProvider,
@SessionDatabase private val monarchy: Monarchy,
private val globalErrorReceiver: GlobalErrorReceiver,
private val getWellknownTask: GetWellknownTask,
Expand All @@ -70,7 +71,6 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (!doRequest) {
monarchy.awaitTransaction { realm ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)

doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
}
}
Expand All @@ -87,7 +87,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(

val mediaConfig = runCatching {
executeRequest(globalErrorReceiver) {
mediaAPI.getMediaConfig()
mediaAPIProvider.getMediaAPI().getMediaConfig()
bmarty marked this conversation as resolved.
Show resolved Hide resolved
}
}.getOrNull()

Expand Down Expand Up @@ -155,6 +155,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
getVersionResult.doesServerSupportRemoteToggleOfPushNotifications()
homeServerCapabilitiesEntity.canRedactEventWithRelations =
getVersionResult.doesServerSupportRedactionOfRelatedEvents()
homeServerCapabilitiesEntity.canUseAuthenticatedMedia =
getVersionResult.doesServerSupportAuthenticatedMedia()
}

if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
Expand Down
Loading
Loading