From ea9df46c70a1b806c11198a1272aac99dc8b62f4 Mon Sep 17 00:00:00 2001 From: Kegsay Date: Fri, 3 Jul 2020 17:24:51 +0100 Subject: [PATCH] Implement local redaction (#1182) * Create redaction events and apply checks (but do not send them) * Send redactions to the roomserver * Linting * Slightly better wording --- clientapi/routing/createroom.go | 2 +- clientapi/routing/membership.go | 6 +- clientapi/routing/redaction.go | 136 ++++++++++++++++++++++++++++++ clientapi/routing/routing.go | 9 ++ clientapi/routing/sendevent.go | 4 +- currentstateserver/api/wrapper.go | 18 ++++ federationapi/routing/join.go | 2 +- federationapi/routing/leave.go | 13 +-- internal/eventutil/events.go | 25 +++--- roomserver/api/wrapper.go | 17 ++++ sytest-whitelist | 4 + 11 files changed, 206 insertions(+), 30 deletions(-) create mode 100644 clientapi/routing/redaction.go diff --git a/clientapi/routing/createroom.go b/clientapi/routing/createroom.go index b6a5d1221..027a21e76 100644 --- a/clientapi/routing/createroom.go +++ b/clientapi/routing/createroom.go @@ -382,7 +382,7 @@ func createRoom( continue } // Build some stripped state for the invite. - candidates := append(gomatrixserverlib.UnwrapEventHeaders(builtEvents), *inviteEvent) + candidates := append(gomatrixserverlib.UnwrapEventHeaders(builtEvents), inviteEvent.Event) var strippedState []gomatrixserverlib.InviteV2StrippedState for _, event := range candidates { switch event.Type() { diff --git a/clientapi/routing/membership.go b/clientapi/routing/membership.go index c2145159a..a9a8fa00d 100644 --- a/clientapi/routing/membership.go +++ b/clientapi/routing/membership.go @@ -77,7 +77,7 @@ func sendMembership(ctx context.Context, accountDB accounts.Database, device *us _, err = roomserverAPI.SendEvents( ctx, rsAPI, - []gomatrixserverlib.HeaderedEvent{event.Headered(roomVer)}, + []gomatrixserverlib.HeaderedEvent{event.Event.Headered(roomVer)}, cfg.Matrix.ServerName, nil, ) @@ -210,7 +210,7 @@ func SendInvite( perr := roomserverAPI.SendInvite( req.Context(), rsAPI, - event.Headered(roomVer), + event.Event.Headered(roomVer), nil, // ask the roomserver to draw up invite room state for us cfg.Matrix.ServerName, nil, @@ -232,7 +232,7 @@ func buildMembershipEvent( membership, roomID string, isDirect bool, cfg *config.Dendrite, evTime time.Time, rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, -) (*gomatrixserverlib.Event, error) { +) (*gomatrixserverlib.HeaderedEvent, error) { profile, err := loadProfile(ctx, targetUserID, cfg, accountDB, asAPI) if err != nil { return nil, err diff --git a/clientapi/routing/redaction.go b/clientapi/routing/redaction.go new file mode 100644 index 000000000..fd80e0ab4 --- /dev/null +++ b/clientapi/routing/redaction.go @@ -0,0 +1,136 @@ +// Copyright 2020 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 routing + +import ( + "context" + "net/http" + "time" + + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + currentstateAPI "github.com/matrix-org/dendrite/currentstateserver/api" + "github.com/matrix-org/dendrite/internal/config" + "github.com/matrix-org/dendrite/internal/eventutil" + "github.com/matrix-org/dendrite/roomserver/api" + roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" + userapi "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" +) + +type redactionContent struct { + Reason string `json:"reason"` +} + +type redactionResponse struct { + EventID string `json:"event_id"` +} + +func SendRedaction( + req *http.Request, device *userapi.Device, roomID, eventID string, cfg *config.Dendrite, + rsAPI roomserverAPI.RoomserverInternalAPI, stateAPI currentstateAPI.CurrentStateInternalAPI, +) util.JSONResponse { + resErr := checkMemberInRoom(req.Context(), stateAPI, device.UserID, roomID) + if resErr != nil { + return *resErr + } + + ev := roomserverAPI.GetEvent(req.Context(), rsAPI, eventID) + if ev == nil { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.NotFound("unknown event ID"), // TODO: is it ok to leak existence? + } + } + if ev.RoomID() != roomID { + return util.JSONResponse{ + Code: 400, + JSON: jsonerror.NotFound("cannot redact event in another room"), + } + } + + // "Users may redact their own events, and any user with a power level greater than or equal + // to the redact power level of the room may redact events there" + // https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid + allowedToRedact := ev.Sender() == device.UserID + if !allowedToRedact { + plEvent := currentstateAPI.GetEvent(req.Context(), stateAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomPowerLevels, + StateKey: "", + }) + if plEvent == nil { + return util.JSONResponse{ + Code: 403, + JSON: jsonerror.Forbidden("You don't have permission to redact this event, no power_levels event in this room."), + } + } + pl, err := plEvent.PowerLevels() + if err != nil { + return util.JSONResponse{ + Code: 403, + JSON: jsonerror.Forbidden( + "You don't have permission to redact this event, the power_levels event for this room is malformed so auth checks cannot be performed.", + ), + } + } + allowedToRedact = pl.UserLevel(device.UserID) >= pl.Redact + } + if !allowedToRedact { + return util.JSONResponse{ + Code: 403, + JSON: jsonerror.Forbidden("You don't have permission to redact this event, power level too low."), + } + } + + var r redactionContent + resErr = httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return *resErr + } + + // create the new event and set all the fields we can + builder := gomatrixserverlib.EventBuilder{ + Sender: device.UserID, + RoomID: roomID, + Type: gomatrixserverlib.MRoomRedaction, + Redacts: eventID, + } + err := builder.SetContent(r) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Error("builder.SetContent failed") + return jsonerror.InternalServerError() + } + + var queryRes api.QueryLatestEventsAndStateResponse + e, err := eventutil.BuildEvent(req.Context(), &builder, cfg, time.Now(), rsAPI, &queryRes) + if err == eventutil.ErrRoomNoExists { + return util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound("Room does not exist"), + } + } + _, err = roomserverAPI.SendEvents(context.Background(), rsAPI, []gomatrixserverlib.HeaderedEvent{*e}, cfg.Matrix.ServerName, nil) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Errorf("failed to SendEvents") + return jsonerror.InternalServerError() + } + return util.JSONResponse{ + Code: 200, + JSON: redactionResponse{ + EventID: e.EventID(), + }, + } +} diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 754fbca80..fe4f1efaa 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -338,6 +338,15 @@ func Setup( return SendTyping(req, device, vars["roomID"], vars["userID"], accountDB, eduAPI, stateAPI) }), ).Methods(http.MethodPut, http.MethodOptions) + r0mux.Handle("/rooms/{roomID}/redact/{eventID}", + httputil.MakeAuthAPI("rooms_redact", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + return SendRedaction(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI, stateAPI) + }), + ).Methods(http.MethodPost, http.MethodOptions) r0mux.Handle("/sendToDevice/{eventType}/{txnID}", httputil.MakeAuthAPI("send_to_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { diff --git a/clientapi/routing/sendevent.go b/clientapi/routing/sendevent.go index aba5f0d51..bf32992f5 100644 --- a/clientapi/routing/sendevent.go +++ b/clientapi/routing/sendevent.go @@ -180,11 +180,11 @@ func generateSendEvent( stateEvents[i] = &queryRes.StateEvents[i].Event } provider := gomatrixserverlib.NewAuthEvents(stateEvents) - if err = gomatrixserverlib.Allowed(*e, &provider); err != nil { + if err = gomatrixserverlib.Allowed(e.Event, &provider); err != nil { return nil, &util.JSONResponse{ Code: http.StatusForbidden, JSON: jsonerror.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client? } } - return e, nil + return &e.Event, nil } diff --git a/currentstateserver/api/wrapper.go b/currentstateserver/api/wrapper.go index 9f7486a02..c88740c0d 100644 --- a/currentstateserver/api/wrapper.go +++ b/currentstateserver/api/wrapper.go @@ -7,6 +7,24 @@ import ( "github.com/matrix-org/util" ) +// GetEvent returns the current state event in the room or nil. +func GetEvent(ctx context.Context, stateAPI CurrentStateInternalAPI, roomID string, tuple gomatrixserverlib.StateKeyTuple) *gomatrixserverlib.HeaderedEvent { + var res QueryCurrentStateResponse + err := stateAPI.QueryCurrentState(ctx, &QueryCurrentStateRequest{ + RoomID: roomID, + StateTuples: []gomatrixserverlib.StateKeyTuple{tuple}, + }, &res) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("Failed to QueryCurrentState") + return nil + } + ev, ok := res.StateEvents[tuple] + if ok { + return ev + } + return nil +} + // PopulatePublicRooms extracts PublicRoom information for all the provided room IDs. The IDs are not checked to see if they are visible in the // published room directory. // due to lots of switches diff --git a/federationapi/routing/join.go b/federationapi/routing/join.go index 8dcd15333..17981f532 100644 --- a/federationapi/routing/join.go +++ b/federationapi/routing/join.go @@ -118,7 +118,7 @@ func MakeJoin( } provider := gomatrixserverlib.NewAuthEvents(stateEvents) - if err = gomatrixserverlib.Allowed(*event, &provider); err != nil { + if err = gomatrixserverlib.Allowed(event.Event, &provider); err != nil { return util.JSONResponse{ Code: http.StatusForbidden, JSON: jsonerror.Forbidden(err.Error()), diff --git a/federationapi/routing/leave.go b/federationapi/routing/leave.go index 108fc50ae..56f1b05af 100644 --- a/federationapi/routing/leave.go +++ b/federationapi/routing/leave.go @@ -32,15 +32,6 @@ func MakeLeave( rsAPI api.RoomserverInternalAPI, roomID, userID string, ) util.JSONResponse { - verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID} - verRes := api.QueryRoomVersionForRoomResponse{} - if err := rsAPI.QueryRoomVersionForRoom(httpReq.Context(), &verReq, &verRes); err != nil { - return util.JSONResponse{ - Code: http.StatusInternalServerError, - JSON: jsonerror.InternalServerError(), - } - } - _, domain, err := gomatrixserverlib.SplitID('@', userID) if err != nil { return util.JSONResponse{ @@ -91,7 +82,7 @@ func MakeLeave( stateEvents[i] = &queryRes.StateEvents[i].Event } provider := gomatrixserverlib.NewAuthEvents(stateEvents) - if err = gomatrixserverlib.Allowed(*event, &provider); err != nil { + if err = gomatrixserverlib.Allowed(event.Event, &provider); err != nil { return util.JSONResponse{ Code: http.StatusForbidden, JSON: jsonerror.Forbidden(err.Error()), @@ -101,7 +92,7 @@ func MakeLeave( return util.JSONResponse{ Code: http.StatusOK, JSON: map[string]interface{}{ - "room_version": verRes.RoomVersion, + "room_version": event.RoomVersion, "event": builder, }, } diff --git a/internal/eventutil/events.go b/internal/eventutil/events.go index e6c7a4ff7..d56f5be8f 100644 --- a/internal/eventutil/events.go +++ b/internal/eventutil/events.go @@ -40,12 +40,12 @@ func BuildEvent( ctx context.Context, builder *gomatrixserverlib.EventBuilder, cfg *config.Dendrite, evTime time.Time, rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse, -) (*gomatrixserverlib.Event, error) { +) (*gomatrixserverlib.HeaderedEvent, error) { if queryRes == nil { queryRes = &api.QueryLatestEventsAndStateResponse{} } - err := AddPrevEventsToEvent(ctx, builder, rsAPI, queryRes) + ver, err := AddPrevEventsToEvent(ctx, builder, rsAPI, queryRes) if err != nil { // This can pass through a ErrRoomNoExists to the caller return nil, err @@ -59,7 +59,8 @@ func BuildEvent( return nil, err } - return &event, nil + h := event.Headered(ver) + return &h, nil } // AddPrevEventsToEvent fills out the prev_events and auth_events fields in builder @@ -67,14 +68,14 @@ func AddPrevEventsToEvent( ctx context.Context, builder *gomatrixserverlib.EventBuilder, rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse, -) error { +) (gomatrixserverlib.RoomVersion, error) { eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) if err != nil { - return fmt.Errorf("gomatrixserverlib.StateNeededForEventBuilder: %w", err) + return "", fmt.Errorf("gomatrixserverlib.StateNeededForEventBuilder: %w", err) } if len(eventsNeeded.Tuples()) == 0 { - return errors.New("expecting state tuples for event builder, got none") + return "", errors.New("expecting state tuples for event builder, got none") } // Ask the roomserver for information about this room @@ -83,16 +84,16 @@ func AddPrevEventsToEvent( StateToFetch: eventsNeeded.Tuples(), } if err = rsAPI.QueryLatestEventsAndState(ctx, &queryReq, queryRes); err != nil { - return fmt.Errorf("rsAPI.QueryLatestEventsAndState: %w", err) + return "", fmt.Errorf("rsAPI.QueryLatestEventsAndState: %w", err) } if !queryRes.RoomExists { - return ErrRoomNoExists + return "", ErrRoomNoExists } eventFormat, err := queryRes.RoomVersion.EventFormat() if err != nil { - return fmt.Errorf("queryRes.RoomVersion.EventFormat: %w", err) + return "", fmt.Errorf("queryRes.RoomVersion.EventFormat: %w", err) } builder.Depth = queryRes.Depth @@ -102,13 +103,13 @@ func AddPrevEventsToEvent( for i := range queryRes.StateEvents { err = authEvents.AddEvent(&queryRes.StateEvents[i].Event) if err != nil { - return fmt.Errorf("authEvents.AddEvent: %w", err) + return "", fmt.Errorf("authEvents.AddEvent: %w", err) } } refs, err := eventsNeeded.AuthEventReferences(&authEvents) if err != nil { - return fmt.Errorf("eventsNeeded.AuthEventReferences: %w", err) + return "", fmt.Errorf("eventsNeeded.AuthEventReferences: %w", err) } truncAuth, truncPrev := truncateAuthAndPrevEvents(refs, queryRes.LatestEvents) @@ -128,7 +129,7 @@ func AddPrevEventsToEvent( builder.PrevEvents = v2PrevRefs } - return nil + return queryRes.RoomVersion, nil } // truncateAuthAndPrevEvents limits the number of events we add into diff --git a/roomserver/api/wrapper.go b/roomserver/api/wrapper.go index b73cd1902..b6a4c8888 100644 --- a/roomserver/api/wrapper.go +++ b/roomserver/api/wrapper.go @@ -18,6 +18,7 @@ import ( "context" "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" ) // SendEvents to the roomserver The events are written with KindNew. @@ -115,3 +116,19 @@ func SendInvite( } return nil } + +// GetEvent returns the event or nil, even on errors. +func GetEvent(ctx context.Context, rsAPI RoomserverInternalAPI, eventID string) *gomatrixserverlib.HeaderedEvent { + var res QueryEventsByIDResponse + err := rsAPI.QueryEventsByID(ctx, &QueryEventsByIDRequest{ + EventIDs: []string{eventID}, + }, &res) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("Failed to QueryEventsByID") + return nil + } + if len(res.Events) != 1 { + return nil + } + return &res.Events[0] +} diff --git a/sytest-whitelist b/sytest-whitelist index 85517cf9a..30380af0e 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -289,6 +289,10 @@ Inbound federation can return events Inbound federation can return missing events for world_readable visibility Inbound federation can return missing events for invite visibility Inbound federation can get public room list +POST /rooms/:room_id/redact/:event_id as power user redacts message +POST /rooms/:room_id/redact/:event_id as original message sender redacts message +POST /rooms/:room_id/redact/:event_id as random user does not redact message +POST /redact disallows redaction of event in different room An event which redacts itself should be ignored A pair of events which redact each other should be ignored Outbound federation can backfill events