complement/tests/restricted_rooms_test.go
strawberry 14cc5be797
add back join_authorised_via_users_server correctly formatted
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-12-15 17:59:56 -05:00

446 lines
15 KiB
Go

// Tests MSC3083, joining restricted rooms based on membership in another room.
package tests
import (
"testing"
"github.com/tidwall/gjson"
"github.com/matrix-org/complement"
"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/client"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/match"
"github.com/matrix-org/complement/must"
"github.com/matrix-org/complement/runtime"
)
// Creates two rooms on room version 8 and sets the second room to have
// restricted join rules with allow set to the first room.
func setupRestrictedRoom(t *testing.T, deployment complement.Deployment, roomVersion string, joinRule string) (*client.CSAPI, string, string) {
t.Helper()
alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{})
// The room which membership checks are delegated to. In practice, this will
// often be an MSC1772 space, but that is not required.
allowed_room := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": "Allowed Room",
})
// The room is room version 8 which supports the restricted join_rule.
room := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": "Room",
"room_version": roomVersion,
"initial_state": []map[string]interface{}{
{
"type": "m.room.join_rules",
"state_key": "",
"content": map[string]interface{}{
"join_rule": joinRule,
"allow": []map[string]interface{}{
{
"type": "m.room_membership",
"room_id": &allowed_room,
"via": []string{"hs1"},
},
},
},
},
},
})
return alice, allowed_room, room
}
func checkRestrictedRoom(t *testing.T, alice *client.CSAPI, bob *client.CSAPI, allowed_room string, room string, joinRule string) {
t.Helper()
t.Run("Join should fail initially", func(t *testing.T) {
res := bob.JoinRoom(t, room, []string{"hs1"})
must.MatchFailure(t, res)
})
t.Run("Join should succeed when joined to allowed room", func(t *testing.T) {
// Join the allowed room.
bob.JoinRoom(t, allowed_room, []string{"hs1"})
// Confirm that we joined the allowed room by changing displayname and
// waiting for confirmation in the /sync response. (This is an attempt
// to mitigate race conditions between Synapse workers. We want to
// ensure that the worker serving the join to `room` knows we are joined
// to `allowed_room`.)
bob.SendEventSynced(
t,
allowed_room,
b.Event{
Type: "m.room.member",
Sender: bob.UserID,
StateKey: &bob.UserID,
Content: map[string]interface{}{
"membership": "join",
"displayname": "Bobby",
},
},
)
// We should now be able to join the restricted room.
bob.JoinRoom(t, room, []string{"hs1"})
// Joining the same room again should work fine (e.g. to change your display name).
bob.SendEventSynced(
t,
room,
b.Event{
Type: "m.room.member",
Sender: bob.UserID,
StateKey: &bob.UserID,
Content: map[string]interface{}{
"membership": "join",
"displayname": "Bobby",
// This should be ignored by the server since this is a join -> join transition
"join_authorised_via_users_server": "@unused:unused.local",
},
},
)
})
t.Run("Join should fail when left allowed room", func(t *testing.T) {
// Leaving the room works and the user is unable to re-join.
bob.MustLeaveRoom(t, room)
bob.MustLeaveRoom(t, allowed_room)
// Wait until Alice sees Bob leave the allowed room. This ensures that Alice's HS
// has processed the leave before Bob tries rejoining, so that it rejects his
// attempt to join the room.
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(
allowed_room, func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.member" || ev.Get("sender").Str != bob.UserID {
return false
}
return ev.Get("content").Get("membership").Str == "leave"
}))
res := bob.JoinRoom(t, room, []string{"hs1"})
must.MatchFailure(t, res)
})
t.Run("Join should succeed when invited", func(t *testing.T) {
// Invite the user and joining should work.
alice.MustInviteRoom(t, room, bob.UserID)
bob.JoinRoom(t, room, []string{"hs1"})
// Leave the room again, and join the allowed room.
bob.MustLeaveRoom(t, room)
bob.JoinRoom(t, allowed_room, []string{"hs1"})
})
t.Run("Join should fail with mangled join rules", func(t *testing.T) {
// Update the room to have bad values in the "allow" field, which should stop
// joining from working properly.
emptyStateKey := ""
alice.SendEventSynced(
t,
room,
b.Event{
Type: "m.room.join_rules",
Sender: alice.UserID,
StateKey: &emptyStateKey,
Content: map[string]interface{}{
"join_rule": joinRule,
"allow": []string{"invalid"},
},
},
)
// Fails since invalid values get filtered out of allow.
must.MatchFailure(t, bob.JoinRoom(t, room, []string{"hs1"}))
alice.SendEventSynced(
t,
room,
b.Event{
Type: "m.room.join_rules",
Sender: alice.UserID,
StateKey: &emptyStateKey,
Content: map[string]interface{}{
"join_rule": joinRule,
"allow": "invalid",
},
},
)
// Fails since a fully invalid allow key requires an invite.
must.MatchFailure(t, bob.JoinRoom(t, room, []string{"hs1"}))
})
}
// Test joining a room with join rules restricted to membership in another room.
func TestRestrictedRoomsLocalJoin(t *testing.T) {
deployment := complement.Deploy(t, 1)
defer deployment.Destroy(t)
// Setup the user, allowed room, and restricted room.
alice, allowed_room, room := setupRestrictedRoom(t, deployment, "8", "restricted")
// Create a second user on the same homeserver.
bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{})
// Execute the checks.
checkRestrictedRoom(t, alice, bob, allowed_room, room, "restricted")
}
// Test joining a room with join rules restricted to membership in another room.
func TestRestrictedRoomsRemoteJoin(t *testing.T) {
deployment := complement.Deploy(t, 2)
defer deployment.Destroy(t)
// Setup the user, allowed room, and restricted room.
alice, allowed_room, room := setupRestrictedRoom(t, deployment, "8", "restricted")
// Create a second user on a different homeserver.
bob := deployment.Register(t, "hs2", helpers.RegistrationOpts{})
// Execute the checks.
checkRestrictedRoom(t, alice, bob, allowed_room, room, "restricted")
}
// A server will do a remote join for a local user if it is unable to to issue
// joins in a restricted room it is already participating in.
func TestRestrictedRoomsRemoteJoinLocalUser(t *testing.T) {
doTestRestrictedRoomsRemoteJoinLocalUser(t, "8", "restricted")
}
func doTestRestrictedRoomsRemoteJoinLocalUser(t *testing.T, roomVersion string, joinRule string) {
runtime.SkipIf(t, runtime.Dendrite) // FIXME: https://github.com/matrix-org/dendrite/issues/2801
deployment := complement.Deploy(t, 2)
defer deployment.Destroy(t)
// Charlie sets up the allowed room so it is on the other server.
//
// This is the room which membership checks are delegated to. In practice,
// this will often be an MSC1772 space, but that is not required.
charlie := deployment.Register(t, "hs2", helpers.RegistrationOpts{})
allowed_room := charlie.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": "Space",
})
// The room is room version 8 which supports the restricted join_rule.
room := charlie.MustCreateRoom(t, map[string]interface{}{
"preset": "public_chat",
"name": "Room",
"room_version": roomVersion,
"initial_state": []map[string]interface{}{
{
"type": "m.room.join_rules",
"state_key": "",
"content": map[string]interface{}{
"join_rule": joinRule,
"allow": []map[string]interface{}{
{
"type": "m.room_membership",
"room_id": &allowed_room,
"via": []string{"hs2"},
},
},
},
},
},
})
// Invite alice manually and accept it.
alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{})
charlie.MustInviteRoom(t, room, alice.UserID)
alice.JoinRoom(t, room, []string{"hs2"})
// Confirm that Alice cannot issue invites (due to the default power levels).
bob := deployment.Register(t, "hs1", helpers.RegistrationOpts{})
res := alice.InviteRoom(t, room, bob.UserID)
must.MatchResponse(t, res, match.HTTPResponse{
StatusCode: 403,
})
// Bob cannot join the room.
must.MatchFailure(t, bob.JoinRoom(t, room, []string{"hs1"}))
// Join the allowed room via hs2.
bob.JoinRoom(t, allowed_room, []string{"hs2"})
// Joining the room should work, although we're joining via hs1, it will end up
// as a remote join through hs2.
bob.JoinRoom(t, room, []string{"hs1"})
// Ensure that the join comes down sync on hs2. Note that we want to ensure hs2
// accepted the event.
charlie.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(
room,
func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != bob.UserID {
return false
}
must.Equal(t, ev.Get("sender").Str, bob.UserID, "Bob should have joined by himself")
must.Equal(t, ev.Get("content").Get("membership").Str, "join", "Bob failed to join the room")
return true
},
))
// Raise the power level so that users on hs1 can invite people and then leave
// the room.
state_key := ""
charlie.SendEventSynced(t, room, b.Event{
Type: "m.room.power_levels",
StateKey: &state_key,
Content: map[string]interface{}{
"invite": 0,
"users": map[string]interface{}{
charlie.UserID: 100,
},
},
})
charlie.MustLeaveRoom(t, room)
// Ensure the events have synced to hs1.
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(
room,
func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID {
return false
}
must.Equal(t, ev.Get("content").Get("membership").Str, "leave", "Charlie failed to leave the room")
return true
},
))
// Have bob leave and rejoin. This should still work even though hs2 isn't in
// the room anymore!
bob.MustLeaveRoom(t, room)
bob.JoinRoom(t, room, []string{"hs1"})
}
// A server will request a failover if asked to /make_join and it does not have
// the appropriate authorisation to complete the request.
//
// Setup 3 homeservers:
// * hs1 creates the allowed room/room.
// * hs2 joins the room
// * hs3 attempts to join via hs2 (should fail) and hs1 (should work)
func TestRestrictedRoomsRemoteJoinFailOver(t *testing.T) {
doTestRestrictedRoomsRemoteJoinFailOver(t, "8", "restricted")
}
func doTestRestrictedRoomsRemoteJoinFailOver(t *testing.T, roomVersion string, joinRule string) {
runtime.SkipIf(t, runtime.Dendrite) // FIXME: https://github.com/matrix-org/dendrite/issues/2801
deployment := complement.Deploy(t, 3)
defer deployment.Destroy(t)
// Setup the user, allowed room, and restricted room.
alice, allowed_room, room := setupRestrictedRoom(t, deployment, roomVersion, joinRule)
t.Logf("%s created authorizing room %s.", alice.UserID, allowed_room)
t.Logf("%s created restricted room %s.", alice.UserID, room)
// Raise the power level so that only alice can invite.
t.Logf("%s restricts invites to themself only.", alice.UserID)
state_key := ""
alice.SendEventSynced(t, room, b.Event{
Type: "m.room.power_levels",
StateKey: &state_key,
Content: map[string]interface{}{
"invite": 100,
"users": map[string]interface{}{
alice.UserID: 100,
},
},
})
// Create a second user on a different homeserver.
bob := deployment.Register(t, "hs2", helpers.RegistrationOpts{})
// Bob joins the room and allowed room.
t.Logf("%s joins the authorizing room via hs1.", bob.UserID)
t.Logf("%s joins the restricted room via hs1.", bob.UserID)
bob.JoinRoom(t, allowed_room, []string{"hs1"})
bob.JoinRoom(t, room, []string{"hs1"})
// Charlie should join the allowed room (which gives access to the room).
charlie := deployment.Register(t, "hs3", helpers.RegistrationOpts{})
t.Logf("%s joins the authorizing room via hs1.", charlie.UserID)
charlie.JoinRoom(t, allowed_room, []string{"hs1"})
// hs2 doesn't have anyone to invite from, so the join fails.
t.Logf("%s joins the restricted room via hs2, which is expected to fail.", charlie.UserID)
res := charlie.JoinRoom(t, room, []string{"hs2"})
must.MatchFailure(t, res)
// Including hs1 (and failing over to it) allows the join to succeed.
t.Logf("%s joins the restricted room via {hs2,hs1}.", charlie.UserID)
charlie.JoinRoom(t, room, []string{"hs2", "hs1"})
// Double check that the join was authorised via hs1.
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(
room,
func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID {
return false
}
must.Equal(t, ev.Get("content").Get("membership").Str, "join", "Charlie failed to join the room")
must.Equal(t, ev.Get("content").Get("join_authorised_via_users_server").Str, alice.UserID, "Join authorised via incorrect server")
return true
},
))
// Bump the power-level of bob.
t.Logf("%s allows %s to send invites.", alice.UserID, bob.UserID)
alice.SendEventSynced(t, room, b.Event{
Type: "m.room.power_levels",
StateKey: &state_key,
Content: map[string]interface{}{
"invite": 100,
"users": map[string]interface{}{
alice.UserID: 100,
bob.UserID: 100,
},
},
})
// Charlie leaves the room (so they can rejoin).
t.Logf("%s leaves the restricted room.", charlie.UserID)
charlie.MustLeaveRoom(t, room)
// Ensure the events have synced to hs1 and hs2, otherwise the joins below may
// happen before the leaves, from the perspective of hs1 and hs2.
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(charlie.UserID, room))
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncLeftFrom(charlie.UserID, room))
// Bob leaves the allowed room so that hs2 doesn't know if Charlie is in the
// allowed room or not.
t.Logf("%s leaves the authorizing room.", bob.UserID)
bob.MustLeaveRoom(t, allowed_room)
// hs2 cannot complete the join since they do not know if Charlie meets the
// requirements (since it is no longer in the allowed room).
t.Logf("%s joins the restricted room via hs2, which is expected to fail.", charlie.UserID)
must.MatchFailure(t, charlie.JoinRoom(t, room, []string{"hs2"}))
// Including hs1 (and failing over to it) allows the join to succeed.
t.Logf("%s joins the restricted room via {hs2,hs1}.", charlie.UserID)
charlie.JoinRoom(t, room, []string{"hs2", "hs1"})
// Double check that the join was authorised via hs1.
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHas(
room,
func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.member" || ev.Get("state_key").Str != charlie.UserID {
return false
}
must.MatchGJSON(t, ev,
match.JSONKeyEqual("content.membership", "join"),
match.JSONKeyEqual("content.join_authorised_via_users_server", alice.UserID),
)
return true
},
))
}