Skip to content

Commit

Permalink
Merge pull request #3739 from vector-im/feature/bca/accept_unbound_3p…
Browse files Browse the repository at this point in the history
…id_invite

support email invite
  • Loading branch information
bmarty authored Aug 27, 2021
2 parents 5b2478a + 67295c6 commit 65c8ae3
Show file tree
Hide file tree
Showing 28 changed files with 512 additions and 33 deletions.
1 change: 1 addition & 0 deletions changelog.d/3691.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support accept 3pid invite when email is not bound to account
1 change: 1 addition & 0 deletions changelog.d/3695.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update Email invite to be aware of spaces
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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

import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser

@FixMethodOrder(MethodSorters.JVM)
class PermalinkParserTest {

@Test
fun testParseEmailInvite() {
val rawInvite = """
https://app.element.io/#/room/%21MRBNLPtFnMAazZVPMO%3Amatrix.org?email=bob%2Bspace%40example.com&signurl=https%3A%2F%2Fvector.im%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3DXmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe%26private_key%3DT2gq0c3kJB_8OroXVxl1pBnzHsN7V6Xn4bEBSeW1ep4&room_name=Team2&room_avatar_url=&inviter_name=hiphop5&guest_access_token=&guest_user_id=
""".trimIndent()
.replace("https://app.element.io/#/room/", "https://matrix.to/#/")

val parsedLink = PermalinkParser.parse(rawInvite)
Assert.assertTrue("Should be parsed as email invite but was ${parsedLink::class.java}", parsedLink is PermalinkData.RoomEmailInviteLink)
parsedLink as PermalinkData.RoomEmailInviteLink
Assert.assertEquals("!MRBNLPtFnMAazZVPMO:matrix.org", parsedLink.roomId)
Assert.assertEquals("XmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe", parsedLink.token)
Assert.assertEquals("vector.im", parsedLink.identityServer)
Assert.assertEquals("Team2", parsedLink.roomName)
Assert.assertEquals("hiphop5", parsedLink.inviterName)
}

@Test
fun testParseLinkWIthEvent() {
val rawInvite = "https://matrix.to/#/!OGEhHVWSdvArJzumhm:matrix.org/\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc?via=matrix.org&via=libera.chat&via=matrix.example.io"

val parsedLink = PermalinkParser.parse(rawInvite)
Assert.assertTrue("Should be parsed as room link", parsedLink is PermalinkData.RoomLink)
parsedLink as PermalinkData.RoomLink
Assert.assertEquals("!OGEhHVWSdvArJzumhm:matrix.org", parsedLink.roomIdOrAlias)
Assert.assertEquals("\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc", parsedLink.eventId)
Assert.assertEquals(3, parsedLink.viaParameters.size)
Assert.assertTrue(parsedLink.viaParameters.contains("matrix.example.io"))
Assert.assertTrue(parsedLink.viaParameters.contains("matrix.org"))
Assert.assertTrue(parsedLink.viaParameters.contains("matrix.example.io"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.matrix.android.sdk.api.session.identity

import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult

/**
* Provides access to the identity server configuration and services identity server can provide
*/
Expand Down Expand Up @@ -121,6 +123,18 @@ interface IdentityService {
*/
suspend fun getShareStatus(threePids: List<ThreePid>): Map<ThreePid, SharedState>

/**
* When one performs a 3pid invite and the third party identifier is unknown, the home server
* will store the invitation in the Identity server and store some information in the room state membership event.
* The email invite will contains the token and secret that can be used to claim the stored invitation
*
* To aid clients who may not be able to perform crypto themselves,
* the identity server offers some crypto functionality to help in accepting invitations.
* This is less secure than the client doing it itself, but may be useful where this isn't possible.
*/
suspend fun sign3pidInvitation(identiyServer: String, token: String, secret: String) : SignInvitationResult

fun addListener(listener: IdentityServiceListener)

fun removeListener(listener: IdentityServiceListener)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package org.matrix.android.sdk.api.session.permalinks

import android.net.Uri
import android.os.Parcelable
import kotlinx.parcelize.Parcelize

/**
* This sealed class represents all the permalink cases.
Expand All @@ -31,6 +33,25 @@ sealed class PermalinkData {
val viaParameters: List<String>
) : PermalinkData()

/**
* &room_name=Team2
&room_avatar_url=mxc:
&inviter_name=bob
*/
@Parcelize
data class RoomEmailInviteLink(
val roomId: String,
val email: String,
val signUrl: String,
val roomName: String?,
val roomAvatarUrl: String?,
val inviterName: String?,
val identityServer: String,
val token: String,
val privateKey: String,
val roomType: String?
) : PermalinkData(), Parcelable

data class UserLink(val userId: String) : PermalinkData()

data class GroupLink(val groupId: String) : PermalinkData()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package org.matrix.android.sdk.api.session.permalinks
import android.net.Uri
import android.net.UrlQuerySanitizer
import org.matrix.android.sdk.api.MatrixPatterns
import timber.log.Timber
import java.net.URLDecoder

/**
* This class turns an uri to a [PermalinkData]
Expand All @@ -35,12 +37,15 @@ object PermalinkParser {

/**
* Turns an uri to a [PermalinkData]
* /~https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/
fun parse(uri: Uri): PermalinkData {
if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) {
return PermalinkData.FallbackLink(uri)
}
val fragment = uri.fragment
// We can't use uri.fragment as it is decoding to early and it will break the parsing
// of parameters that represents url (like signurl)
val fragment = uri.toString().substringAfter("#") // uri.fragment
if (fragment.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri)
}
Expand All @@ -51,6 +56,7 @@ object PermalinkParser {
val params = safeFragment
.split(MatrixPatterns.SEP_REGEX)
.filter { it.isNotEmpty() }
.map { URLDecoder.decode(it, "UTF-8") }
.take(2)

val identifier = params.getOrNull(0)
Expand All @@ -60,12 +66,7 @@ object PermalinkParser {
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters
)
handleRoomIdCase(fragment, identifier, uri, extraParameter, viaQueryParameters)
}
MatrixPatterns.isRoomAlias(identifier) -> {
PermalinkData.RoomLink(
Expand All @@ -79,13 +80,59 @@ object PermalinkParser {
}
}

private fun handleRoomIdCase(fragment: String, identifier: String, uri: Uri, extraParameter: String?, viaQueryParameters: List<String>): PermalinkData {
// Can't rely on built in parsing because it's messing around the signurl
val paramList = safeExtractParams(fragment)
val signUrl = paramList.firstOrNull { it.first == "signurl" }?.second
val email = paramList.firstOrNull { it.first == "email" }?.second
return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) {
try {
val signValidUri = Uri.parse(signUrl)
val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException()
val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException()
val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException()
PermalinkData.RoomEmailInviteLink(
roomId = identifier,
email = email!!,
signUrl = signUrl!!,
roomName = paramList.firstOrNull { it.first == "room_name" }?.second,
inviterName = paramList.firstOrNull { it.first == "inviter_name" }?.second,
roomAvatarUrl = paramList.firstOrNull { it.first == "room_avatar_url" }?.second,
roomType = paramList.firstOrNull { it.first == "room_type" }?.second,
identityServer = identityServerHost,
token = token,
privateKey = privateKey
)
} catch (failure: Throwable) {
Timber.i("## Permalink: Failed to parse permalink $signUrl")
PermalinkData.FallbackLink(uri)
}
} else {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters
)
}
}

private fun safeExtractParams(fragment: String) = fragment.substringAfter("?").split('&').mapNotNull {
val splitNameValue = it.split("=")
if (splitNameValue.size == 2) {
Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8"))
} else null
}

private fun String.getViaParameters(): List<String> {
return UrlQuerySanitizer(this)
.parameterList
.filter {
it.mParameter == "via"
}.map {
it.mValue
it.mValue.let {
URLDecoder.decode(it, "UTF-8")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription

/**
Expand Down Expand Up @@ -63,6 +64,18 @@ interface RoomService {
reason: String? = null,
viaServers: List<String> = emptyList())

/**
* @param roomId the roomId of the room to join
* @param reason optional reason for joining the room
* @param thirdPartySigned A signature of an m.third_party_invite token to prove that this user owns a third party identity
* which has been invited to the room.
*/
suspend fun joinRoom(
roomId: String,
reason: String? = null,
thirdPartySigned: SignInvitationResult
)

/**
* Get a room from a roomId
* @param roomId the roomId to look for.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal object NetworkConstants {
// Identity server
const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2"
const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/"
const val URI_IDENTITY_PATH_V1 = "_matrix/identity/api/v1/"

// Push Gateway
const val URI_PUSH_GATEWAY_PREFIX_PATH = "_matrix/push/v1/"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import timber.log.Timber
import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
Expand Down Expand Up @@ -79,6 +80,7 @@ internal class DefaultIdentityService @Inject constructor(
private val identityApiProvider: IdentityApiProvider,
private val accountDataDataSource: UserAccountDataDataSource,
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
private val sign3pidInvitationTask: DefaultSign3pidInvitationTask,
private val sessionParams: SessionParams
) : IdentityService, SessionLifecycleObserver {

Expand Down Expand Up @@ -290,6 +292,14 @@ internal class DefaultIdentityService @Inject constructor(
return token.token
}

override suspend fun sign3pidInvitation(identiyServer: String, token: String, secret: String): SignInvitationResult {
return sign3pidInvitationTask.execute(Sign3pidInvitationTask.Params(
url = identiyServer,
token = token,
privateKey = secret
))
}

override fun addListener(listener: IdentityServiceListener) {
listeners.add(listener)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwn
import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody
import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody
import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query

/**
* Ref: https://matrix.org/docs/spec/identity_service/latest
Expand Down Expand Up @@ -95,4 +97,16 @@ internal interface IdentityAPI {
@POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken")
suspend fun submitToken(@Path("medium") medium: String,
@Body body: IdentityRequestOwnershipParams): SuccessResult

/**
* https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-sign-ed25519
*
* Have to rely on V1 for now
*/
@POST(NetworkConstants.URI_IDENTITY_PATH_V1 + "sign-ed25519")
suspend fun signInvitationDetails(
@Query("token") token: String,
@Query("private_key") privateKey: String,
@Query("mxid") mxid: String
): SignInvitationResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* 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.session.identity

import dagger.Lazy
import okhttp3.OkHttpClient
import org.matrix.android.sdk.internal.di.Unauthenticated
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.RetrofitFactory
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject

internal interface Sign3pidInvitationTask : Task<Sign3pidInvitationTask.Params, SignInvitationResult> {
data class Params(
val token: String,
val url: String,
val privateKey: String
)
}

internal class DefaultSign3pidInvitationTask @Inject constructor(
@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
@UserId private val userId: String
) : Sign3pidInvitationTask {

override suspend fun execute(params: Sign3pidInvitationTask.Params): SignInvitationResult {
val identityAPI = retrofitFactory
.create(okHttpClient, "https://${params.url}")
.create(IdentityAPI::class.java)
return identityAPI.signInvitationDetails(params.token, params.privateKey, userId)
}
}
Loading

0 comments on commit 65c8ae3

Please sign in to comment.