Merge pull request #3694 from element-hq/feature/bma/oidcPrompt

OIDC prompt
This commit is contained in:
Benoit Marty 2024-10-17 09:16:02 +02:00 committed by GitHub
commit 80a052c7cf
37 changed files with 202 additions and 142 deletions

View file

@ -43,7 +43,7 @@ interface MatrixAuthenticationService {
/**
* Get the Oidc url to display to the user.
*/
suspend fun getOidcUrl(): Result<OidcDetails>
suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails>
/**
* Cancel Oidc login sequence.

View file

@ -0,0 +1,51 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
sealed interface OidcPrompt {
/**
* The Authorization Server must not display any authentication or consent
* user interface pages.
*/
data object None : OidcPrompt
/**
* The Authorization Server should prompt the End-User for
* reauthentication.
*/
data object Login : OidcPrompt
/**
* The Authorization Server should prompt the End-User for consent before
* returning information to the Client.
*/
data object Consent : OidcPrompt
/**
* The Authorization Server should prompt the End-User to select a user
* account.
*
* This enables an End-User who has multiple accounts at the Authorization
* Server to select amongst the multiple accounts that they might have
* current sessions for.
*/
data object SelectAccount : OidcPrompt
/**
* The Authorization Server should prompt the End-User to create a user
* account.
*
* Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
*/
data object Create : OidcPrompt
/**
* An unknown value.
*/
data class Unknown(val value: String) : OidcPrompt
}

View file

@ -25,14 +25,14 @@ interface MatrixMediaLoader {
/**
* @param source to fetch the data for.
* @param mimeType: optional mime type.
* @param body: optional body which will be used to name the file.
* @param filename: optional String which will be used to name the file.
* @param useCache: if true, the rust sdk will cache the media in its store.
* @return a [Result] of [MediaFile]
*/
suspend fun downloadMediaFile(
source: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
useCache: Boolean = true,
): Result<MediaFile>
}

View file

@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.room
enum class CurrentUserMembership {
INVITED,
JOINED,
LEFT
LEFT,
KNOCKED,
}

View file

@ -18,7 +18,6 @@ data class EventTimelineItem(
val transactionId: TransactionId?,
val isEditable: Boolean,
val canBeRepliedTo: Boolean,
val isLocal: Boolean,
val isOwn: Boolean,
val isRemote: Boolean,
val localSendState: LocalEventSendState?,
@ -28,9 +27,9 @@ data class EventTimelineItem(
val senderProfile: ProfileTimelineDetails,
val timestamp: Long,
val content: EventContent,
val debugInfoProvider: EventDebugInfoProvider,
val origin: TimelineItemEventOrigin?,
val messageShieldProvider: EventShieldsProvider,
val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
val messageShieldProvider: MessageShieldProvider,
) {
fun inReplyTo(): InReplyTo? {
return (content as? MessageContent)?.inReplyTo
@ -46,10 +45,10 @@ data class EventTimelineItem(
}
}
fun interface EventDebugInfoProvider {
fun get(): TimelineItemDebugInfo
fun interface TimelineItemDebugInfoProvider {
operator fun invoke(): TimelineItemDebugInfo
}
fun interface EventShieldsProvider {
fun getShield(strict: Boolean): MessageShield?
fun interface MessageShieldProvider {
operator fun invoke(strict: Boolean): MessageShield?
}

View file

@ -0,0 +1,22 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt
internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt {
return when (this) {
OidcPrompt.None -> RustOidcPrompt.None
OidcPrompt.Login -> RustOidcPrompt.Login
OidcPrompt.Consent -> RustOidcPrompt.Consent
OidcPrompt.SelectAccount -> RustOidcPrompt.SelectAccount
OidcPrompt.Create -> RustOidcPrompt.Create
is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value)
}
}

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
@ -181,11 +182,14 @@ class RustMatrixAuthenticationService @Inject constructor(
private var pendingOidcAuthorizationData: OidcAuthorizationData? = null
override suspend fun getOidcUrl(): Result<OidcDetails> {
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> {
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oidcAuthenticationData = client.urlForOidcLogin(oidcConfigurationProvider.get())
val oidcAuthenticationData = client.urlForOidc(
oidcConfiguration = oidcConfigurationProvider.get(),
prompt = prompt.toRustPrompt(),
)
val url = oidcAuthenticationData.loginUrl()
pendingOidcAuthorizationData = oidcAuthenticationData
OidcDetails(url)

View file

@ -63,7 +63,7 @@ class RustMediaLoader(
override suspend fun downloadMediaFile(
source: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
useCache: Boolean,
): Result<MediaFile> =
withContext(mediaDispatcher) {
@ -71,7 +71,7 @@ class RustMediaLoader(
source.toRustMediaSource().use { mediaSource ->
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
body = body,
filename = filename,
mimeType = mimeType?.takeIf { MimeTypes.hasSubtype(it) } ?: MimeTypes.OctetStream,
useCache = useCache,
tempDir = cacheDirectory.path,

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomHero
import org.matrix.rustcomponents.sdk.Membership as RustMembership
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
@ -65,6 +66,7 @@ fun RustMembership.map(): CurrentUserMembership = when (this) {
RustMembership.INVITED -> CurrentUserMembership.INVITED
RustMembership.JOINED -> CurrentUserMembership.JOINED
RustMembership.LEFT -> CurrentUserMembership.LEFT
Membership.KNOCKED -> CurrentUserMembership.KNOCKED
}
fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {

View file

@ -12,9 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventDebugInfoProvider
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.EventShieldsProvider
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
@ -27,12 +25,10 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.EventOrTransactionId
import org.matrix.rustcomponents.sdk.EventSendState
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfoProvider
import org.matrix.rustcomponents.sdk.Reaction
import org.matrix.rustcomponents.sdk.ShieldState
import uniffi.matrix_sdk_common.ShieldStateCode
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
import org.matrix.rustcomponents.sdk.EventShieldsProvider as RustEventShieldsProvider
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails
@ -48,7 +44,6 @@ class EventTimelineItemMapper(
transactionId = eventOrTransactionId.transactionId(),
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
isLocal = isLocal,
isOwn = isOwn,
isRemote = isRemote,
localSendState = localSendState?.map(),
@ -58,9 +53,9 @@ class EventTimelineItemMapper(
senderProfile = senderProfile.map(),
timestamp = timestamp.toLong(),
content = contentMapper.map(content),
debugInfoProvider = RustEventDebugInfoProvider(debugInfoProvider),
origin = origin?.map(),
messageShieldProvider = RustEventShieldsProvider(shieldsProvider)
timelineItemDebugInfoProvider = { lazyProvider.debugInfo().map() },
messageShieldProvider = { strict -> lazyProvider.getShields(strict)?.map() },
)
}
}
@ -168,18 +163,6 @@ private fun ShieldState?.map(): MessageShield? {
}
}
class RustEventDebugInfoProvider(private val debugInfoProvider: EventTimelineItemDebugInfoProvider) : EventDebugInfoProvider {
override fun get(): TimelineItemDebugInfo {
return debugInfoProvider.get().map()
}
}
class RustEventShieldsProvider(private val shieldsProvider: RustEventShieldsProvider) : EventShieldsProvider {
override fun getShield(strict: Boolean): MessageShield? {
return shieldsProvider.getShields(strict)?.map()
}
}
private fun EventOrTransactionId.eventId(): EventId? {
return (this as? EventOrTransactionId.EventId)?.let { EventId(it.eventId) }
}

View file

@ -7,8 +7,7 @@
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustEventShieldsProvider
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustEventTimelineItemDebugInfoProvider
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustLazyTimelineItemProvider
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import org.matrix.rustcomponents.sdk.EventOrTransactionId
@ -23,7 +22,6 @@ import org.matrix.rustcomponents.sdk.TimelineItemContent
import uniffi.matrix_sdk_ui.EventItemOrigin
fun aRustEventTimelineItem(
isLocal: Boolean = false,
isRemote: Boolean = true,
eventOrTransactionId: EventOrTransactionId = EventOrTransactionId.EventId(AN_EVENT_ID.value),
sender: String = A_USER_ID.value,
@ -40,7 +38,6 @@ fun aRustEventTimelineItem(
canBeRepliedTo: Boolean = true,
shieldsState: ShieldState? = null,
) = EventTimelineItem(
isLocal = isLocal,
isRemote = isRemote,
eventOrTransactionId = eventOrTransactionId,
sender = sender,
@ -50,10 +47,12 @@ fun aRustEventTimelineItem(
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
content = content,
debugInfoProvider = FakeRustEventTimelineItemDebugInfoProvider(debugInfo),
shieldsProvider = FakeRustEventShieldsProvider(shieldsState),
localSendState = localSendState,
reactions = reactions,
readReceipts = readReceipts,
origin = origin,
lazyProvider = FakeRustLazyTimelineItemProvider(
debugInfo = debugInfo,
shieldsState = shieldsState,
)
)

View file

@ -1,18 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.EventShieldsProvider
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.ShieldState
class FakeRustEventShieldsProvider(
private val shieldsState: ShieldState? = null,
) : EventShieldsProvider(NoPointer) {
override fun getShields(strict: Boolean): ShieldState? = shieldsState
}

View file

@ -9,11 +9,14 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.libraries.matrix.impl.fixtures.factories.anEventTimelineItemDebugInfo
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfoProvider
import org.matrix.rustcomponents.sdk.LazyTimelineItemProvider
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.ShieldState
class FakeRustEventTimelineItemDebugInfoProvider(
class FakeRustLazyTimelineItemProvider(
private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
) : EventTimelineItemDebugInfoProvider(NoPointer) {
override fun get(): EventTimelineItemDebugInfo = debugInfo
private val shieldsState: ShieldState? = null,
) : LazyTimelineItemProvider(NoPointer) {
override fun getShields(strict: Boolean) = shieldsState
override fun debugInfo() = debugInfo
}

View file

@ -11,6 +11,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.OidcPrompt
import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
@ -80,7 +81,7 @@ class FakeMatrixAuthenticationService(
return importCreatedSessionLambda(externalSession)
}
override suspend fun getOidcUrl(): Result<OidcDetails> = simulateLongTask {
override suspend fun getOidcUrl(prompt: OidcPrompt): Result<OidcDetails> = simulateLongTask {
oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA)
}

View file

@ -35,7 +35,7 @@ class FakeMatrixMediaLoader : MatrixMediaLoader {
override suspend fun downloadMediaFile(
source: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
useCache: Boolean,
): Result<MediaFile> = simulateLongTask {
if (shouldFail) {

View file

@ -42,7 +42,6 @@ fun anEventTimelineItem(
transactionId: TransactionId? = null,
isEditable: Boolean = false,
canBeRepliedTo: Boolean = false,
isLocal: Boolean = false,
isOwn: Boolean = false,
isRemote: Boolean = false,
localSendState: LocalEventSendState? = null,
@ -59,7 +58,6 @@ fun anEventTimelineItem(
transactionId = transactionId,
isEditable = isEditable,
canBeRepliedTo = canBeRepliedTo,
isLocal = isLocal,
isOwn = isOwn,
isRemote = isRemote,
localSendState = localSendState,
@ -69,8 +67,8 @@ fun anEventTimelineItem(
senderProfile = senderProfile,
timestamp = timestamp,
content = content,
debugInfoProvider = { debugInfo },
origin = null,
timelineItemDebugInfoProvider = { debugInfo },
messageShieldProvider = { messageShield },
)

View file

@ -92,7 +92,7 @@ class MediaViewerPresenter @AssistedInject constructor(
mediaLoader.downloadMediaFile(
source = inputs.mediaSource,
mimeType = inputs.mediaInfo.mimeType,
body = inputs.mediaInfo.filename
filename = inputs.mediaInfo.filename
)
.onSuccess {
mediaFile.value = it

View file

@ -299,7 +299,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
.getMediaFile(
mediaSource = messageType.source,
mimeType = messageType.info?.mimetype,
body = messageType.filename,
filename = messageType.filename,
)
is VideoMessageType -> null // Use the thumbnail here?
else -> null

View file

@ -47,13 +47,13 @@ interface NotificationMediaRepo {
*
* @param mediaSource the media source of the media.
* @param mimeType the mime type of the media.
* @param body optional body which will be used to name the file.
* @param filename optional String which will be used to name the file.
* @return A [Result] holding either the media [File] from the cache directory or an [Exception].
*/
suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
): Result<File>
}
@ -75,7 +75,7 @@ class DefaultNotificationMediaRepo @AssistedInject constructor(
override suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
): Result<File> {
val cachedFile = mediaSource.cachedFile()
return when {
@ -84,7 +84,7 @@ class DefaultNotificationMediaRepo @AssistedInject constructor(
else -> matrixMediaLoader.downloadMediaFile(
source = mediaSource,
mimeType = mimeType,
body = body,
filename = filename,
).mapCatching {
it.use { mediaFile ->
val dest = cachedFile.apply { parentFile?.mkdirs() }

View file

@ -15,7 +15,7 @@ class FakeNotificationMediaRepo : NotificationMediaRepo {
override suspend fun getMediaFile(
mediaSource: MediaSource,
mimeType: String?,
body: String?,
filename: String?,
): Result<File> {
return Result.failure(IllegalStateException("Fake class"))
}