Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
86 changes: 86 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ func (c *CSAPI) CreateMedia(t ct.TestLike) string {
return GetJSONFieldStr(t, body, "content_uri")
}

// CreateMedia creates an MXC URI for asynchronous media uploads.
func (c *CSAPI) MustCreateMediaRestricted(t ct.TestLike) string {
t.Helper()
res := c.MustDo(t, "POST", []string{"_matrix", "client", "unstable", "org.matrix.msc3911", "media", "create"})
body := ParseJSON(t, res)
return GetJSONFieldStr(t, body, "content_uri")
}

// UploadMediaAsync uploads the provided content to the given server and media ID. Fails the test on error.
func (c *CSAPI) UploadMediaAsync(t ct.TestLike, serverName, mediaID string, fileBody []byte, fileName string, contentType string) {
t.Helper()
Expand Down Expand Up @@ -99,6 +107,23 @@ func (c *CSAPI) UploadContent(t ct.TestLike, fileBody []byte, fileName string, c
return GetJSONFieldStr(t, body, "content_uri")
}

// MustUploadContentRestricted uploads the provided content with and attachment parameter and an optional file name. Fails the test on error. Returns the MXC URI.
func (c *CSAPI) MustUploadContentRestricted(t ct.TestLike, fileBody []byte, fileName string, contentType string) string {
t.Helper()
query := url.Values{}
if fileName != "" {
query.Set("filename", fileName)
}
res := c.Do(
// /_matrix/client/unstable/org.matrix.msc3911/media/upload
t, "POST", []string{"_matrix", "client", "unstable", "org.matrix.msc3911", "media", "upload"},
WithRawBody(fileBody), WithContentType(contentType), WithQueries(query),
)
mustRespond2xx(t, res)
body := ParseJSON(t, res)
return GetJSONFieldStr(t, body, "content_uri")
}

// DownloadContent downloads media from the server, returning the raw bytes and the Content-Type. Fails the test on error.
func (c *CSAPI) DownloadContent(t ct.TestLike, mxcUri string) ([]byte, string) {
t.Helper()
Expand All @@ -125,6 +150,16 @@ func (c *CSAPI) DownloadContentAuthenticated(t ct.TestLike, mxcUri string) ([]by
return b, contentType
}

// UncheckedDownloadContentAuthenticated makes the raw request for a piece of media and returns the http.Response.
// Response is unchecked in any way. The existing DownloadContentAuthenticated() should have been a "Must" variant. Rather
// than refactor that across the code base, this version just uses an explict name
func (c *CSAPI) UncheckedDownloadContentAuthenticated(t ct.TestLike, mxcUri string) *http.Response {
t.Helper()
origin, mediaId := SplitMxc(mxcUri)
res := c.Do(t, "GET", []string{"_matrix", "client", "v1", "media", "download", origin, mediaId})
return res
}

// MustCreateRoom creates a room with an optional HTTP request body. Fails the test on error. Returns the room ID.
func (c *CSAPI) MustCreateRoom(t ct.TestLike, reqBody map[string]interface{}) string {
t.Helper()
Expand Down Expand Up @@ -337,6 +372,15 @@ func (c *CSAPI) Unsafe_SendEventUnsynced(t ct.TestLike, roomID string, e b.Event
return c.Unsafe_SendEventUnsyncedWithTxnID(t, roomID, e, strconv.Itoa(txnID))
}

// Unsafe_SendEventWithAttachedMediaUnsynced sends `e` with a media attachment into the room. This function is UNSAFE as it does not wait
// for the event to be fully processed. This can cause flakey tests. Prefer `SendEventSynced`.
// Returns the event ID of the sent event.
func (c *CSAPI) Unsafe_SendEventWithAttachedMediaUnsynced(t ct.TestLike, roomID string, e b.Event, mxcUri string) string {
t.Helper()
txnID := int(atomic.AddInt64(&c.txnID, 1))
return c.Unsafe_SendEventWithAttachedMediaUnsyncedWithTxnID(t, roomID, e, mxcUri, strconv.Itoa(txnID))
}

// SendEventUnsyncedWithTxnID sends `e` into the room with a prescribed transaction ID.
// This is useful for writing tests that interrogate transaction semantics. This function is UNSAFE
// as it does not wait for the event to be fully processed. This can cause flakey tests. Prefer `SendEventSynced`.
Expand All @@ -356,6 +400,29 @@ func (c *CSAPI) Unsafe_SendEventUnsyncedWithTxnID(t ct.TestLike, roomID string,
return eventID
}

// Unsafe_SendEventWithAttachedMediaUnsyncedWithTxnID sends `e` with a media attachment into the room with a prescribed transaction ID.
// This is useful for writing tests that interrogate transaction semantics. This function is UNSAFE
// as it does not wait for the event to be fully processed. This can cause flakey tests. Prefer `SendEventSynced`.
// Returns the event ID of the sent event.
func (c *CSAPI) Unsafe_SendEventWithAttachedMediaUnsyncedWithTxnID(t ct.TestLike, roomID string, e b.Event, mxcUri string, txnID string) string {
t.Helper()
paths := []string{"_matrix", "client", "v3", "rooms", roomID, "send", e.Type, txnID}
if e.StateKey != nil {
paths = []string{"_matrix", "client", "v3", "rooms", roomID, "state", e.Type, *e.StateKey}
}
if e.Sender != "" && e.Sender != c.UserID {
ct.Fatalf(t, "Event.Sender must not be set, as this is set by the client in use (%s)", c.UserID)
}
queries := url.Values{}
if mxcUri != "" {
queries.Add("org.matrix.msc3911.attach_media", mxcUri)
}
res := c.MustDo(t, "PUT", paths, WithJSONBody(t, e.Content), WithQueries(queries))
body := ParseJSON(t, res)
eventID := GetJSONFieldStr(t, body, "event_id")
return eventID
}

// SendEventSynced sends `e` into the room and waits for its event ID to come down /sync.
// Returns the event ID of the sent event.
func (c *CSAPI) SendEventSynced(t ct.TestLike, roomID string, e b.Event) string {
Expand All @@ -368,6 +435,18 @@ func (c *CSAPI) SendEventSynced(t ct.TestLike, roomID string, e b.Event) string
return eventID
}

// SendEventWithAttachedMediaSynced sends `e` with a media attachment into the room and waits for its event ID to come down /sync.
// Returns the event ID of the sent event.
func (c *CSAPI) SendEventWithAttachedMediaSynced(t ct.TestLike, roomID string, e b.Event, mxcUri string) string {
t.Helper()
eventID := c.Unsafe_SendEventWithAttachedMediaUnsynced(t, roomID, e, mxcUri)
t.Logf("SendEventSynced waiting for event ID %s", eventID)
c.MustSyncUntil(t, SyncReq{}, SyncTimelineHas(roomID, func(r gjson.Result) bool {
return r.Get("event_id").Str == eventID
}))
return eventID
}

// SendRedaction sends a redaction request. Will fail if the returned HTTP request code is not 200. Returns the
// event ID of the redaction event.
func (c *CSAPI) MustSendRedaction(t ct.TestLike, roomID string, content map[string]interface{}, eventID string) string {
Expand Down Expand Up @@ -550,6 +629,13 @@ func (c *CSAPI) MustSetDisplayName(t ct.TestLike, displayname string) {
}))
}

// MustSetDisplayName sets the global display name for this account or fails the test.
func (c *CSAPI) MustSetProfileAvatar(t ct.TestLike, mxcUri string) {
c.MustDo(t, "PUT", []string{"_matrix", "client", "v3", "profile", c.UserID, "avatar_url"}, WithJSONBody(t, map[string]any{
"avatar_url": mxcUri,
}))
}

// MustGetDisplayName returns the global display name for this user or fails the test.
func (c *CSAPI) MustGetDisplayName(t ct.TestLike, userID string) string {
res := c.MustDo(t, "GET", []string{"_matrix", "client", "v3", "profile", userID, "displayname"})
Expand Down
237 changes: 237 additions & 0 deletions tests/msc3911/cs_api_restricted_media_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package tests

import (
"bytes"
"testing"

"github.com/matrix-org/complement"
"github.com/matrix-org/complement/client"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/internal/data"
"github.com/matrix-org/complement/match"
"github.com/matrix-org/complement/must"
"github.com/matrix-org/gomatrixserverlib/spec"
)

// This test series is for checking behavior of MSC3911 - Linking Media to Events

func TestRestrictedMediaUnstable(t *testing.T) {
deployment := complement.Deploy(t, 2)
defer deployment.Destroy(t)

alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{
LocalpartSuffix: "alice",
Password: "password",
})
bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{
LocalpartSuffix: "bob",
Password: "password",
})
charlie := deployment.Register(t, "hs1", helpers.RegistrationOpts{
LocalpartSuffix: "charlie",
Password: "password",
})

alice.LoginUser(t, alice.UserID, "password")

// Let's give alice a profile avatar
aliceGlobalProfileAvatarMxcUri := alice.MustUploadContentRestricted(t, data.MatrixSvg, "alice_avatar.svg", "img/svg")
alice.MustSetProfileAvatar(t, aliceGlobalProfileAvatarMxcUri)

// Test that the profile that was just set is viewable and not restricted
aliceOriginalProfileBytes, _ := alice.DownloadContentAuthenticated(t, aliceGlobalProfileAvatarMxcUri)
bob.LoginUser(t, bob.UserID, "password")
charlie.LoginUser(t, charlie.UserID, "password")

// The first four test check that a message event, such as an image, is restricted correctly in different room history
// visibility scenarios

// Test media uploaded to a "joined" history visibility room is only viewable by the room's joined participants
t.Run("TestJoinedVisibilityMediaMessage", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "joined")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
bob.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

mxcUri, message_event_id := MustUploadMediaAttachToMessageEventAndSendIntoRoom(t, alice, roomID)

bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, message_event_id))

// bob should have the right to see that media
bob.DownloadContentAuthenticated(t, mxcUri)

// charlie is a little trouble maker and found alice's picture mxc uri
// Since charlie can not see into the room, the media is not downloadable
response := charlie.UncheckedDownloadContentAuthenticated(t, mxcUri)
must.MatchResponse(t, response, match.HTTPResponse{StatusCode: 403})
})

// Test media uploaded to a "invited" history visibility room is only viewable by the room's invited participants
t.Run("TestInvitedVisibilityMediaMessage", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "invited")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))

mxcUri, _ := MustUploadMediaAttachToMessageEventAndSendIntoRoom(t, alice, roomID)

// bob should have the right to see that media
bob.DownloadContentAuthenticated(t, mxcUri)

// charlie is a little trouble maker and found alice's picture mxc uri
// Since charlie can not see into the room, the media is not downloadable
response := charlie.UncheckedDownloadContentAuthenticated(t, mxcUri)
must.MatchResponse(t, response, match.HTTPResponse{StatusCode: 403})

})

// Test media uploaded to a "shared" history visibility room is viewable by any one(this is wrong, but how it is with Synapse)
t.Run("TestSharedVisibilityMediaMessage", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "shared")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
bob.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

mxcUri, message_event_id := MustUploadMediaAttachToMessageEventAndSendIntoRoom(t, alice, roomID)

bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, message_event_id))

// bob should have the right to see that media
bob.DownloadContentAuthenticated(t, mxcUri)

// because of the shared visibility being incorrect in synapse, this is viewable
charlie.DownloadContentAuthenticated(t, mxcUri)
})

// Test media uploaded to a "world_viewable" history visibility room is viewable by any one
t.Run("TestWorldReadableVisibilityMediaMessage", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "world_readable")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
bob.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

mxcUri, message_event_id := MustUploadMediaAttachToMessageEventAndSendIntoRoom(t, alice, roomID)

bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, message_event_id))

// This is a world_readable room, everyone can see this media
bob.DownloadContentAuthenticated(t, mxcUri)
charlie.DownloadContentAuthenticated(t, mxcUri)
})

// The MembershipAvatar series tests that media restricted to a membership state event is viewable.

// Test profile avatar for membership event is only viewable by joined room participants
t.Run("TestJoinedVisibilityMembershipAvatar", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "joined")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
bob.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

mxcUri, membership_event_id := MustUploadMediaAttachToMembershipEventAndSendIntoRoom(t, alice, roomID)

bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, membership_event_id))

// bob should have the right to see that media
bob.DownloadContentAuthenticated(t, mxcUri)

// charlie is a little trouble maker and found alice's picture mxc uri
// Since charlie can not see into the room, the media is not downloadable
response := charlie.UncheckedDownloadContentAuthenticated(t, mxcUri)
must.MatchResponse(t, response, match.HTTPResponse{StatusCode: 403})
})

// Test profile avatar for membership event is only viewable by invited room participants
t.Run("TestInvitedVisibilityMembershipAvatar", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "invited")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
// Notice that bob is not joined to the room

mxcUri, _ := MustUploadMediaAttachToMembershipEventAndSendIntoRoom(t, alice, roomID)

// bob should have the right to see that media. Not sure how he got the mxc without being in the room, but it is allowed.
bob.DownloadContentAuthenticated(t, mxcUri)

// charlie is a little trouble maker and found alice's picture mxc uri
// Since charlie can not see into the room, the media is not downloadable
response := charlie.UncheckedDownloadContentAuthenticated(t, mxcUri)
must.MatchResponse(t, response, match.HTTPResponse{StatusCode: 403})
})

// Test profile avatar for membership event is viewable by room participants
t.Run("TestSharedVisibilityMembershipAvatar", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "shared")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
bob.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

mxcUri, membership_event_id := MustUploadMediaAttachToMembershipEventAndSendIntoRoom(t, alice, roomID)

bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, membership_event_id))

// bob should have the right to see that media
bob.DownloadContentAuthenticated(t, mxcUri)

// because of the shared visibility being incorrect in synapse, this is viewable
charlie.DownloadContentAuthenticated(t, mxcUri)
})

// Test profile avatar for membership event is viewable by room participants
t.Run("TestWorldReadableVisibilityMembershipAvatar", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "world_readable")

alice.MustInviteRoom(t, roomID, bob.UserID)
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncInvitedTo(bob.UserID, roomID))
bob.MustJoinRoom(t, roomID, []spec.ServerName{
deployment.GetFullyQualifiedHomeserverName(t, "hs1"),
})

mxcUri, membership_event_id := MustUploadMediaAttachToMembershipEventAndSendIntoRoom(t, alice, roomID)

bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(roomID, membership_event_id))

// This is a world_readable room, everyone can see this media
bob.DownloadContentAuthenticated(t, mxcUri)
charlie.DownloadContentAuthenticated(t, mxcUri)
})

// Test global profile avatar is copied for initial membership event in a room. This will end up being viewable by anyone(thanks
// to Synapse shared view bug), that is not what is being tested here, so use the most restricive history visibility.
// Note: the global profile that was established on login is the SVG file and our membership state tests above use the PNG file.
t.Run("TestRoomCreationMembershipAvatarIsACopy", func(t *testing.T) {
roomID := MustCreateRoomWithHistoryVisibility(t, alice, "joined")
// Retrieve alice's membership event so we can pry the avatar_url out of it
aliceMemberEvent := alice.MustGetStateEventContent(t, roomID, "m.room.member", alice.UserID)

avatarUrl := aliceMemberEvent.Get("avatar_url").Str

// make sure it is different than the global profile mxc
if avatarUrl == aliceGlobalProfileAvatarMxcUri {
t.Fatalf("mxcUri values should not match for global profile and room membership event")
}

// download it and check that the bytes match
aliceNewAvatarBytes, _ := alice.DownloadContentAuthenticated(t, avatarUrl)
if !bytes.Equal(aliceOriginalProfileBytes, aliceNewAvatarBytes) {
t.Fatalf("Media is differing and should be identical")
}

})
}
Loading