Fix spaces over federation (#3347)

Fixes #2504

 A few issues with the previous iteration:
- We never returned `inaccessible_children`, which (if I read the code
correctly), made Synapse raise an error and thus not returning the
requested rooms
- For restricted rooms, we didn't return the list of allowed rooms
This commit is contained in:
Till 2024-03-28 20:40:45 +01:00 committed by GitHub
parent ad0a7d09e8
commit b732eede27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 68 additions and 37 deletions

View file

@ -138,7 +138,7 @@ func QueryRoomHierarchy(req *http.Request, device *userapi.Device, roomIDStr str
walker = *cachedWalker walker = *cachedWalker
} }
discoveredRooms, nextWalker, err := rsAPI.QueryNextRoomHierarchyPage(req.Context(), walker, limit) discoveredRooms, _, nextWalker, err := rsAPI.QueryNextRoomHierarchyPage(req.Context(), walker, limit)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {

View file

@ -146,7 +146,7 @@ func QueryRoomHierarchy(httpReq *http.Request, request *fclient.FederationReques
} }
walker := roomserverAPI.NewRoomHierarchyWalker(types.NewServerNameNotDevice(request.Origin()), roomID, suggestedOnly, 1) walker := roomserverAPI.NewRoomHierarchyWalker(types.NewServerNameNotDevice(request.Origin()), roomID, suggestedOnly, 1)
discoveredRooms, _, err := rsAPI.QueryNextRoomHierarchyPage(httpReq.Context(), walker, -1) discoveredRooms, inaccessibleRooms, _, err := rsAPI.QueryNextRoomHierarchyPage(httpReq.Context(), walker, -1)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
@ -177,6 +177,7 @@ func QueryRoomHierarchy(httpReq *http.Request, request *fclient.FederationReques
JSON: fclient.RoomHierarchyResponse{ JSON: fclient.RoomHierarchyResponse{
Room: discoveredRooms[0], Room: discoveredRooms[0],
Children: discoveredRooms[1:], Children: discoveredRooms[1:],
InaccessibleChildren: inaccessibleRooms,
}, },
} }
} }

2
go.mod
View file

@ -22,7 +22,7 @@ require (
github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530
github.com/matrix-org/gomatrixserverlib v0.0.0-20240109180417-3495e573f2b7 github.com/matrix-org/gomatrixserverlib v0.0.0-20240326183347-e8077abf519a
github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7 github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7
github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 github.com/matrix-org/util v0.0.0-20221111132719-399730281e66
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.17

4
go.sum
View file

@ -208,8 +208,8 @@ github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 h1:s7fexw
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1:e+cg2q7C7yE5QnAXgzo512tgFh1RbQLC0+jozuegKgo= github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1:e+cg2q7C7yE5QnAXgzo512tgFh1RbQLC0+jozuegKgo=
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U=
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
github.com/matrix-org/gomatrixserverlib v0.0.0-20240109180417-3495e573f2b7 h1:EaUvK2ay6cxMxeshC1p6QswS9+rQFbUc2YerkRFyVXQ= github.com/matrix-org/gomatrixserverlib v0.0.0-20240326183347-e8077abf519a h1:K+lE7Bp2g62Ykfzd9nqxzdXZseOzVWZ494OhQsiLJ1U=
github.com/matrix-org/gomatrixserverlib v0.0.0-20240109180417-3495e573f2b7/go.mod h1:HZGsVJ3bUE+DkZtufkH9H0mlsvbhEGK5CpX0Zlavylg= github.com/matrix-org/gomatrixserverlib v0.0.0-20240326183347-e8077abf519a/go.mod h1:HZGsVJ3bUE+DkZtufkH9H0mlsvbhEGK5CpX0Zlavylg=
github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7 h1:6t8kJr8i1/1I5nNttw6nn1ryQJgzVlBmSGgPiiaTdw4= github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7 h1:6t8kJr8i1/1I5nNttw6nn1ryQJgzVlBmSGgPiiaTdw4=
github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7/go.mod h1:ReWMS/LoVnOiRAdq9sNUC2NZnd1mZkMNB52QhpTRWjg= github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7/go.mod h1:ReWMS/LoVnOiRAdq9sNUC2NZnd1mZkMNB52QhpTRWjg=
github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y=

View file

@ -141,7 +141,12 @@ type QueryRoomHierarchyAPI interface {
// //
// If returned walker is nil, then there are no more rooms left to traverse. This method does not modify the provided walker, so it // If returned walker is nil, then there are no more rooms left to traverse. This method does not modify the provided walker, so it
// can be cached. // can be cached.
QueryNextRoomHierarchyPage(ctx context.Context, walker RoomHierarchyWalker, limit int) ([]fclient.RoomHierarchyRoom, *RoomHierarchyWalker, error) QueryNextRoomHierarchyPage(ctx context.Context, walker RoomHierarchyWalker, limit int) (
hierarchyRooms []fclient.RoomHierarchyRoom,
inaccessibleRooms []string,
hierarchyWalker *RoomHierarchyWalker,
err error,
)
} }
type QueryMembershipAPI interface { type QueryMembershipAPI interface {

View file

@ -189,7 +189,7 @@ func PopulatePublicRooms(ctx context.Context, roomIDs []string, rsAPI QueryBulkS
RoomID: roomID, RoomID: roomID,
} }
joinCount := 0 joinCount := 0
var joinRule, guestAccess string var guestAccess string
for tuple, contentVal := range data { for tuple, contentVal := range data {
if tuple.EventType == spec.MRoomMember && contentVal == "join" { if tuple.EventType == spec.MRoomMember && contentVal == "join" {
joinCount++ joinCount++
@ -210,12 +210,12 @@ func PopulatePublicRooms(ctx context.Context, roomIDs []string, rsAPI QueryBulkS
pub.WorldReadable = contentVal == "world_readable" pub.WorldReadable = contentVal == "world_readable"
// need both of these to determine whether guests can join // need both of these to determine whether guests can join
case joinRuleTuple: case joinRuleTuple:
joinRule = contentVal pub.JoinRule = contentVal
case guestTuple: case guestTuple:
guestAccess = contentVal guestAccess = contentVal
} }
} }
if joinRule == spec.Public && guestAccess == "can_join" { if pub.JoinRule == spec.Public && guestAccess == "can_join" {
pub.GuestCanJoin = true pub.GuestCanJoin = true
} }
pub.JoinedMembersCount = joinCount pub.JoinedMembersCount = joinCount

View file

@ -39,9 +39,14 @@ import (
// //
// If returned walker is nil, then there are no more rooms left to traverse. This method does not modify the provided walker, so it // If returned walker is nil, then there are no more rooms left to traverse. This method does not modify the provided walker, so it
// can be cached. // can be cached.
func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker roomserver.RoomHierarchyWalker, limit int) ([]fclient.RoomHierarchyRoom, *roomserver.RoomHierarchyWalker, error) { func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker roomserver.RoomHierarchyWalker, limit int) (
if authorised, _ := authorised(ctx, querier, walker.Caller, walker.RootRoomID, nil); !authorised { []fclient.RoomHierarchyRoom,
return nil, nil, roomserver.ErrRoomUnknownOrNotAllowed{Err: fmt.Errorf("room is unknown/forbidden")} []string,
*roomserver.RoomHierarchyWalker,
error,
) {
if authorised, _, _ := authorised(ctx, querier, walker.Caller, walker.RootRoomID, nil); !authorised {
return nil, []string{walker.RootRoomID.String()}, nil, roomserver.ErrRoomUnknownOrNotAllowed{Err: fmt.Errorf("room is unknown/forbidden")}
} }
discoveredRooms := []fclient.RoomHierarchyRoom{} discoveredRooms := []fclient.RoomHierarchyRoom{}
@ -50,6 +55,7 @@ func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker r
unvisited := make([]roomserver.RoomHierarchyWalkerQueuedRoom, len(walker.Unvisited)) unvisited := make([]roomserver.RoomHierarchyWalkerQueuedRoom, len(walker.Unvisited))
copy(unvisited, walker.Unvisited) copy(unvisited, walker.Unvisited)
processed := walker.Processed.Copy() processed := walker.Processed.Copy()
inaccessible := []string{}
// Depth first -> stack data structure // Depth first -> stack data structure
for len(unvisited) > 0 { for len(unvisited) > 0 {
@ -108,7 +114,7 @@ func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker r
// as these children may be rooms we do know about. // as these children may be rooms we do know about.
roomType = spec.MSpace roomType = spec.MSpace
} }
} else if authorised, isJoinedOrInvited := authorised(ctx, querier, walker.Caller, queuedRoom.RoomID, queuedRoom.ParentRoomID); authorised { } else if authorised, isJoinedOrInvited, allowedRoomIDs := authorised(ctx, querier, walker.Caller, queuedRoom.RoomID, queuedRoom.ParentRoomID); authorised {
// Get all `m.space.child` state events for this room // Get all `m.space.child` state events for this room
events, err := childReferences(ctx, querier, walker.SuggestedOnly, queuedRoom.RoomID) events, err := childReferences(ctx, querier, walker.SuggestedOnly, queuedRoom.RoomID)
if err != nil { if err != nil {
@ -128,11 +134,15 @@ func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker r
PublicRoom: *pubRoom, PublicRoom: *pubRoom,
RoomType: roomType, RoomType: roomType,
ChildrenState: events, ChildrenState: events,
AllowedRoomIDs: allowedRoomIDs,
}) })
// don't walk children if the user is not joined/invited to the space // don't walk children if the user is not joined/invited to the space
if !isJoinedOrInvited { if !isJoinedOrInvited {
continue continue
} }
} else if !authorised {
inaccessible = append(inaccessible, queuedRoom.RoomID.String())
continue
} else { } else {
// room exists but user is not authorised // room exists but user is not authorised
continue continue
@ -149,6 +159,7 @@ func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker r
// We need to invert the order here because the child events are lo->hi on the timestamp, // We need to invert the order here because the child events are lo->hi on the timestamp,
// so we need to ensure we pop in the same lo->hi order, which won't be the case if we // so we need to ensure we pop in the same lo->hi order, which won't be the case if we
// insert the highest timestamp last in a stack. // insert the highest timestamp last in a stack.
extendQueueLoop:
for i := len(discoveredChildEvents) - 1; i >= 0; i-- { for i := len(discoveredChildEvents) - 1; i >= 0; i-- {
spaceContent := struct { spaceContent := struct {
Via []string `json:"via"` Via []string `json:"via"`
@ -161,6 +172,12 @@ func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker r
if err != nil { if err != nil {
util.GetLogger(ctx).WithError(err).WithField("invalid_room_id", ev.StateKey).WithField("parent_room_id", queuedRoom.RoomID).Warn("Invalid room ID in m.space.child state event") util.GetLogger(ctx).WithError(err).WithField("invalid_room_id", ev.StateKey).WithField("parent_room_id", queuedRoom.RoomID).Warn("Invalid room ID in m.space.child state event")
} else { } else {
// Make sure not to queue inaccessible rooms
for _, inaccessibleRoomID := range inaccessible {
if inaccessibleRoomID == childRoomID.String() {
continue extendQueueLoop
}
}
unvisited = append(unvisited, roomserver.RoomHierarchyWalkerQueuedRoom{ unvisited = append(unvisited, roomserver.RoomHierarchyWalkerQueuedRoom{
RoomID: *childRoomID, RoomID: *childRoomID,
ParentRoomID: &queuedRoom.RoomID, ParentRoomID: &queuedRoom.RoomID,
@ -173,7 +190,7 @@ func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker r
if len(unvisited) == 0 { if len(unvisited) == 0 {
// If no more rooms to walk, then don't return a walker for future pages // If no more rooms to walk, then don't return a walker for future pages
return discoveredRooms, nil, nil return discoveredRooms, inaccessible, nil, nil
} else { } else {
// If there are more rooms to walk, then return a new walker to resume walking from (for querying more pages) // If there are more rooms to walk, then return a new walker to resume walking from (for querying more pages)
newWalker := roomserver.RoomHierarchyWalker{ newWalker := roomserver.RoomHierarchyWalker{
@ -185,22 +202,25 @@ func (querier *Queryer) QueryNextRoomHierarchyPage(ctx context.Context, walker r
Processed: processed, Processed: processed,
} }
return discoveredRooms, &newWalker, nil return discoveredRooms, inaccessible, &newWalker, nil
} }
} }
// authorised returns true iff the user is joined this room or the room is world_readable // authorised returns true iff the user is joined this room or the room is world_readable
func authorised(ctx context.Context, querier *Queryer, caller types.DeviceOrServerName, roomID spec.RoomID, parentRoomID *spec.RoomID) (authed, isJoinedOrInvited bool) { func authorised(ctx context.Context, querier *Queryer, caller types.DeviceOrServerName, roomID spec.RoomID, parentRoomID *spec.RoomID) (authed, isJoinedOrInvited bool, resultAllowedRoomIDs []string) {
if clientCaller := caller.Device(); clientCaller != nil { if clientCaller := caller.Device(); clientCaller != nil {
return authorisedUser(ctx, querier, clientCaller, roomID, parentRoomID) return authorisedUser(ctx, querier, clientCaller, roomID, parentRoomID)
} else {
return authorisedServer(ctx, querier, roomID, *caller.ServerName()), false
} }
if serverCaller := caller.ServerName(); serverCaller != nil {
authed, resultAllowedRoomIDs = authorisedServer(ctx, querier, roomID, *serverCaller)
return authed, false, resultAllowedRoomIDs
}
return false, false, resultAllowedRoomIDs
} }
// authorisedServer returns true iff the server is joined this room or the room is world_readable, public, or knockable // authorisedServer returns true iff the server is joined this room or the room is world_readable, public, or knockable
func authorisedServer(ctx context.Context, querier *Queryer, roomID spec.RoomID, callerServerName spec.ServerName) bool { func authorisedServer(ctx context.Context, querier *Queryer, roomID spec.RoomID, callerServerName spec.ServerName) (bool, []string) {
// Check history visibility / join rules first // Check history visibility / join rules first
hisVisTuple := gomatrixserverlib.StateKeyTuple{ hisVisTuple := gomatrixserverlib.StateKeyTuple{
EventType: spec.MRoomHistoryVisibility, EventType: spec.MRoomHistoryVisibility,
@ -219,13 +239,13 @@ func authorisedServer(ctx context.Context, querier *Queryer, roomID spec.RoomID,
}, &queryRoomRes) }, &queryRoomRes)
if err != nil { if err != nil {
util.GetLogger(ctx).WithError(err).Error("failed to QueryCurrentState") util.GetLogger(ctx).WithError(err).Error("failed to QueryCurrentState")
return false return false, []string{}
} }
hisVisEv := queryRoomRes.StateEvents[hisVisTuple] hisVisEv := queryRoomRes.StateEvents[hisVisTuple]
if hisVisEv != nil { if hisVisEv != nil {
hisVis, _ := hisVisEv.HistoryVisibility() hisVis, _ := hisVisEv.HistoryVisibility()
if hisVis == "world_readable" { if hisVis == "world_readable" {
return true return true, []string{}
} }
} }
@ -238,19 +258,23 @@ func authorisedServer(ctx context.Context, querier *Queryer, roomID spec.RoomID,
rule, ruleErr := joinRuleEv.JoinRule() rule, ruleErr := joinRuleEv.JoinRule()
if ruleErr != nil { if ruleErr != nil {
util.GetLogger(ctx).WithError(ruleErr).WithField("parent_room_id", roomID).Warn("failed to get join rule") util.GetLogger(ctx).WithError(ruleErr).WithField("parent_room_id", roomID).Warn("failed to get join rule")
return false return false, []string{}
} }
if rule == spec.Public || rule == spec.Knock { if rule == spec.Public || rule == spec.Knock {
return true return true, []string{}
} }
if rule == spec.Restricted { if rule == spec.Restricted || rule == spec.KnockRestricted {
allowJoinedToRoomIDs = append(allowJoinedToRoomIDs, restrictedJoinRuleAllowedRooms(ctx, joinRuleEv)...) allowJoinedToRoomIDs = append(allowJoinedToRoomIDs, restrictedJoinRuleAllowedRooms(ctx, joinRuleEv)...)
} }
} }
// check if server is joined to any allowed room // check if server is joined to any allowed room
resultAllowedRoomIDs := make([]string, 0, len(allowJoinedToRoomIDs))
for _, allowedRoomID := range allowJoinedToRoomIDs {
resultAllowedRoomIDs = append(resultAllowedRoomIDs, allowedRoomID.String())
}
for _, allowedRoomID := range allowJoinedToRoomIDs { for _, allowedRoomID := range allowJoinedToRoomIDs {
var queryRes fs.QueryJoinedHostServerNamesInRoomResponse var queryRes fs.QueryJoinedHostServerNamesInRoomResponse
err = querier.FSAPI.QueryJoinedHostServerNamesInRoom(ctx, &fs.QueryJoinedHostServerNamesInRoomRequest{ err = querier.FSAPI.QueryJoinedHostServerNamesInRoom(ctx, &fs.QueryJoinedHostServerNamesInRoomRequest{
@ -262,18 +286,18 @@ func authorisedServer(ctx context.Context, querier *Queryer, roomID spec.RoomID,
} }
for _, srv := range queryRes.ServerNames { for _, srv := range queryRes.ServerNames {
if srv == callerServerName { if srv == callerServerName {
return true return true, resultAllowedRoomIDs[1:]
} }
} }
} }
return false return false, resultAllowedRoomIDs[1:]
} }
// authorisedUser returns true iff the user is invited/joined this room or the room is world_readable // authorisedUser returns true iff the user is invited/joined this room or the room is world_readable
// or if the room has a public or knock join rule. // or if the room has a public or knock join rule.
// Failing that, if the room has a restricted join rule and belongs to the space parent listed, it will return true. // Failing that, if the room has a restricted join rule and belongs to the space parent listed, it will return true.
func authorisedUser(ctx context.Context, querier *Queryer, clientCaller *userapi.Device, roomID spec.RoomID, parentRoomID *spec.RoomID) (authed bool, isJoinedOrInvited bool) { func authorisedUser(ctx context.Context, querier *Queryer, clientCaller *userapi.Device, roomID spec.RoomID, parentRoomID *spec.RoomID) (authed bool, isJoinedOrInvited bool, resultAllowedRoomIDs []string) {
hisVisTuple := gomatrixserverlib.StateKeyTuple{ hisVisTuple := gomatrixserverlib.StateKeyTuple{
EventType: spec.MRoomHistoryVisibility, EventType: spec.MRoomHistoryVisibility,
StateKey: "", StateKey: "",
@ -295,20 +319,20 @@ func authorisedUser(ctx context.Context, querier *Queryer, clientCaller *userapi
}, &queryRes) }, &queryRes)
if err != nil { if err != nil {
util.GetLogger(ctx).WithError(err).Error("failed to QueryCurrentState") util.GetLogger(ctx).WithError(err).Error("failed to QueryCurrentState")
return false, false return false, false, resultAllowedRoomIDs
} }
memberEv := queryRes.StateEvents[roomMemberTuple] memberEv := queryRes.StateEvents[roomMemberTuple]
if memberEv != nil { if memberEv != nil {
membership, _ := memberEv.Membership() membership, _ := memberEv.Membership()
if membership == spec.Join || membership == spec.Invite { if membership == spec.Join || membership == spec.Invite {
return true, true return true, true, resultAllowedRoomIDs
} }
} }
hisVisEv := queryRes.StateEvents[hisVisTuple] hisVisEv := queryRes.StateEvents[hisVisTuple]
if hisVisEv != nil { if hisVisEv != nil {
hisVis, _ := hisVisEv.HistoryVisibility() hisVis, _ := hisVisEv.HistoryVisibility()
if hisVis == "world_readable" { if hisVis == "world_readable" {
return true, false return true, false, resultAllowedRoomIDs
} }
} }
joinRuleEv := queryRes.StateEvents[joinRuleTuple] joinRuleEv := queryRes.StateEvents[joinRuleTuple]
@ -323,6 +347,7 @@ func authorisedUser(ctx context.Context, querier *Queryer, clientCaller *userapi
allowedRoomIDs := restrictedJoinRuleAllowedRooms(ctx, joinRuleEv) allowedRoomIDs := restrictedJoinRuleAllowedRooms(ctx, joinRuleEv)
// check parent is in the allowed set // check parent is in the allowed set
for _, a := range allowedRoomIDs { for _, a := range allowedRoomIDs {
resultAllowedRoomIDs = append(resultAllowedRoomIDs, a.String())
if *parentRoomID == a { if *parentRoomID == a {
allowed = true allowed = true
break break
@ -345,13 +370,13 @@ func authorisedUser(ctx context.Context, querier *Queryer, clientCaller *userapi
if memberEv != nil { if memberEv != nil {
membership, _ := memberEv.Membership() membership, _ := memberEv.Membership()
if membership == spec.Join { if membership == spec.Join {
return true, false return true, false, resultAllowedRoomIDs
} }
} }
} }
} }
} }
return false, false return false, false, resultAllowedRoomIDs
} }
// helper function to fetch a state event // helper function to fetch a state event