diff --git a/clientapi/routing/redaction.go b/clientapi/routing/redaction.go new file mode 100644 index 000000000..1c3a08a6d --- /dev/null +++ b/clientapi/routing/redaction.go @@ -0,0 +1,128 @@ +// 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 ( + "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("cannot redact event, missing pl event"), + } + } + pl, err := plEvent.PowerLevels() + if err != nil { + return util.JSONResponse{ + Code: 403, + JSON: jsonerror.Forbidden("cannot redact event, malformed pl event"), + } + } + allowedToRedact = pl.UserLevel(device.UserID) >= pl.Redact + } + if !allowedToRedact { + return util.JSONResponse{ + Code: 403, + JSON: jsonerror.Forbidden("cannot redact event"), + } + } + + 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"), + } + } + 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/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/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