diff --git a/roomserver/auth/history_visibility.go b/roomserver/auth/history_visibility.go index 24e0edd2..8f2ea9a1 100644 --- a/roomserver/auth/history_visibility.go +++ b/roomserver/auth/history_visibility.go @@ -26,7 +26,8 @@ const ( // TODO: This logic should live in gomatrixserverlib // IsServerAllowed returns true if the server is allowed to see events in the room -// at this particular state. This function implements https://matrix.org/docs/spec/client_server/r0.6.0#id87 +// at this particular state. This function implements a server-based version of +// https://matrix.org/docs/spec/client_server/r0.6.0#id87 func IsServerAllowed( serverName gomatrixserverlib.ServerName, serverCurrentlyInRoom bool, diff --git a/syncapi/internal/history_visibility.go b/syncapi/internal/history_visibility.go index 0e5e031e..0670b044 100644 --- a/syncapi/internal/history_visibility.go +++ b/syncapi/internal/history_visibility.go @@ -17,13 +17,153 @@ package internal import ( "context" + "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" + "github.com/sirupsen/logrus" ) +type queryState interface { + QueryStateAfterEvents( + ctx context.Context, + request *api.QueryStateAfterEventsRequest, + response *api.QueryStateAfterEventsResponse, + ) error + QueryMembershipForUser( + ctx context.Context, + request *api.QueryMembershipForUserRequest, + response *api.QueryMembershipForUserResponse, + ) error +} + // ApplyHistoryVisibilityChecks removes items from the input slice if the user is not allowed // to see these events. +// +// This works by using QueryStateAfterEvents to pull out the history visibility and membership +// events for the user at the time of the each input event, then applying the checks detailled +// at https://matrix.org/docs/spec/client_server/r0.6.0#id87 func ApplyHistoryVisibilityChecks( - ctx context.Context, userID string, events []gomatrixserverlib.HeaderedEvent, + ctx context.Context, rsAPI queryState, userID string, events []gomatrixserverlib.HeaderedEvent, ) []gomatrixserverlib.HeaderedEvent { - return events + result := make([]gomatrixserverlib.HeaderedEvent, 0, len(events)) + currentMemberships := make(map[string]*api.QueryMembershipForUserResponse) // room_id => membership + for _, ev := range events { + queryMembership := currentMemberships[ev.RoomID()] + if queryMembership == nil { + var queryRes api.QueryMembershipForUserResponse + // discard errors, we may not actually need to know their *CURRENT* membership to allow + // them access, so let's continue rather than giving up. + _ = rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{ + RoomID: ev.RoomID(), + UserID: userID, + }, &queryRes) + currentMemberships[ev.RoomID()] = &queryRes + queryMembership = &queryRes + } + currentMembership := "leave" + if queryMembership != nil { + currentMembership = queryMembership.Membership + } + if userAllowedToSeeEvent(ctx, rsAPI, userID, currentMembership, ev) { + result = append(result, ev) + } + } + return result +} + +func userAllowedToSeeEvent(ctx context.Context, rsAPI queryState, userID, currentMembership string, ev gomatrixserverlib.HeaderedEvent) bool { + logger := util.GetLogger(ctx).WithFields(logrus.Fields{ + "requester": userID, + "room_id": ev.RoomID(), + "event_id": ev.EventID(), + }) + var queryRes api.QueryStateAfterEventsResponse + err := rsAPI.QueryStateAfterEvents(ctx, &api.QueryStateAfterEventsRequest{ + RoomID: ev.RoomID(), + PrevEventIDs: ev.PrevEventIDs(), + StateToFetch: []gomatrixserverlib.StateKeyTuple{ + { + EventType: gomatrixserverlib.MRoomMember, + StateKey: userID, + }, + { + EventType: gomatrixserverlib.MRoomHistoryVisibility, + StateKey: "", + }, + }, + }, &queryRes) + if err != nil { + logger.WithError(err).Error("Failed to lookup state of room at event, denying user access to event") + return false + } + if !queryRes.RoomExists { + logger.Error("Unknown room, denying user access to event") + return false + } + if !queryRes.PrevEventsExist { + logger.Error("Failed to lookup state of room at event: missing prev_events for this event, denying user access to event") + return false + } + var membershipEvent, hisVisEvent *gomatrixserverlib.HeaderedEvent + for i := range queryRes.StateEvents { + switch queryRes.StateEvents[i].Type() { + case gomatrixserverlib.MRoomMember: + membershipEvent = &queryRes.StateEvents[i] + case gomatrixserverlib.MRoomHistoryVisibility: + hisVisEvent = &queryRes.StateEvents[i] + } + } + // if they recently joined the room and are requesting events from a long time ago then we expect no membership event + // so default to leave + membership := gomatrixserverlib.Leave + if membershipEvent != nil { + membership, _ = membershipEvent.Membership() + } + return historyVisibilityCheckForEvent(&ev, membership, currentMembership, hisVisEvent) +} + +// Implements https://matrix.org/docs/spec/client_server/r0.6.0#id87 for clients. Not to be confused with a similar +// function in roomserver which is designed for servers. +func historyVisibilityCheckForEvent( + ev *gomatrixserverlib.HeaderedEvent, membership, currentMembership string, hisVisEvent *gomatrixserverlib.HeaderedEvent, +) bool { + // By default if no history_visibility is set, or if the value is not understood, the visibility is assumed to be shared. + visibility := "shared" + knownStates := []string{"invited", "joined", "shared", "world_readable"} + if hisVisEvent != nil { + // ignore errors as that means "the value is not understood". + vis, _ := hisVisEvent.HistoryVisibility() + for _, knownVis := range knownStates { + if vis == knownVis { + visibility = vis + break + } + } + } + + // 1. If the history_visibility was set to world_readable, allow. + if visibility == "world_readable" { + return true + } + // 2. If the user's membership was join, allow. + if membership == "join" { + return true + } + // 3. If history_visibility was set to shared, and the user joined the room at any point after the event was sent, allow. + // TODO: This is actually an annoying calculation to do. We happen to know that these checks are for /messages which + // is usually called whilst you're a room member, so we cheat a bit here by checking if we are currently joined. + // This will miss cases where you are not in the room when the event is sent, then join then leave then hit /messages. + // This is a slightly more conservative solution. We could alternatively use QueryMembershipForUserResponse.HasBeenInRoom + // but this would mean a left user could see NEW messages in a room if the history visibility was set to 'shared', without + // having to join it (which would make many people upset, particularly IRC folks). + if visibility == "shared" && (membership == "join" || currentMembership == "join") { + return true + } + + // 4. If the user's membership was invite, and the history_visibility was set to invited, allow. + if membership == "invite" && visibility == "invited" { + return true + } + + return false } diff --git a/syncapi/routing/messages.go b/syncapi/routing/messages.go index 16ad8a91..37a7b7a4 100644 --- a/syncapi/routing/messages.go +++ b/syncapi/routing/messages.go @@ -243,7 +243,11 @@ func (r *messagesReq) retrieveEvents() ( events = reversed(events) } - events = internal.ApplyHistoryVisibilityChecks(r.ctx, r.requester, events) + events = internal.ApplyHistoryVisibilityChecks(r.ctx, r.rsAPI, r.requester, events) + // If we fitered out all the events from these checks, return early + if len(events) == 0 { + return []gomatrixserverlib.ClientEvent{}, *r.from, *r.to, nil + } // Convert all of the events into client events. clientEvents = gomatrixserverlib.HeaderedToClientEvents(events, gomatrixserverlib.FormatAll)