Implement local redaction (#1182)

* Create redaction events and apply checks (but do not send them)

* Send redactions to the roomserver

* Linting

* Slightly better wording
This commit is contained in:
Kegsay 2020-07-03 17:24:51 +01:00 committed by GitHub
parent 46dbc46f84
commit ea9df46c70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 206 additions and 30 deletions

View file

@ -382,7 +382,7 @@ func createRoom(
continue continue
} }
// Build some stripped state for the invite. // Build some stripped state for the invite.
candidates := append(gomatrixserverlib.UnwrapEventHeaders(builtEvents), *inviteEvent) candidates := append(gomatrixserverlib.UnwrapEventHeaders(builtEvents), inviteEvent.Event)
var strippedState []gomatrixserverlib.InviteV2StrippedState var strippedState []gomatrixserverlib.InviteV2StrippedState
for _, event := range candidates { for _, event := range candidates {
switch event.Type() { switch event.Type() {

View file

@ -77,7 +77,7 @@ func sendMembership(ctx context.Context, accountDB accounts.Database, device *us
_, err = roomserverAPI.SendEvents( _, err = roomserverAPI.SendEvents(
ctx, rsAPI, ctx, rsAPI,
[]gomatrixserverlib.HeaderedEvent{event.Headered(roomVer)}, []gomatrixserverlib.HeaderedEvent{event.Event.Headered(roomVer)},
cfg.Matrix.ServerName, cfg.Matrix.ServerName,
nil, nil,
) )
@ -210,7 +210,7 @@ func SendInvite(
perr := roomserverAPI.SendInvite( perr := roomserverAPI.SendInvite(
req.Context(), rsAPI, req.Context(), rsAPI,
event.Headered(roomVer), event.Event.Headered(roomVer),
nil, // ask the roomserver to draw up invite room state for us nil, // ask the roomserver to draw up invite room state for us
cfg.Matrix.ServerName, cfg.Matrix.ServerName,
nil, nil,
@ -232,7 +232,7 @@ func buildMembershipEvent(
membership, roomID string, isDirect bool, membership, roomID string, isDirect bool,
cfg *config.Dendrite, evTime time.Time, cfg *config.Dendrite, evTime time.Time,
rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI,
) (*gomatrixserverlib.Event, error) { ) (*gomatrixserverlib.HeaderedEvent, error) {
profile, err := loadProfile(ctx, targetUserID, cfg, accountDB, asAPI) profile, err := loadProfile(ctx, targetUserID, cfg, accountDB, asAPI)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -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(),
},
}
}

View file

@ -338,6 +338,15 @@ func Setup(
return SendTyping(req, device, vars["roomID"], vars["userID"], accountDB, eduAPI, stateAPI) return SendTyping(req, device, vars["roomID"], vars["userID"], accountDB, eduAPI, stateAPI)
}), }),
).Methods(http.MethodPut, http.MethodOptions) ).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}", r0mux.Handle("/sendToDevice/{eventType}/{txnID}",
httputil.MakeAuthAPI("send_to_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { httputil.MakeAuthAPI("send_to_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {

View file

@ -180,11 +180,11 @@ func generateSendEvent(
stateEvents[i] = &queryRes.StateEvents[i].Event stateEvents[i] = &queryRes.StateEvents[i].Event
} }
provider := gomatrixserverlib.NewAuthEvents(stateEvents) 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{ return nil, &util.JSONResponse{
Code: http.StatusForbidden, Code: http.StatusForbidden,
JSON: jsonerror.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client? JSON: jsonerror.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client?
} }
} }
return e, nil return &e.Event, nil
} }

View file

@ -7,6 +7,24 @@ import (
"github.com/matrix-org/util" "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 // 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. // published room directory.
// due to lots of switches // due to lots of switches

View file

@ -118,7 +118,7 @@ func MakeJoin(
} }
provider := gomatrixserverlib.NewAuthEvents(stateEvents) provider := gomatrixserverlib.NewAuthEvents(stateEvents)
if err = gomatrixserverlib.Allowed(*event, &provider); err != nil { if err = gomatrixserverlib.Allowed(event.Event, &provider); err != nil {
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusForbidden, Code: http.StatusForbidden,
JSON: jsonerror.Forbidden(err.Error()), JSON: jsonerror.Forbidden(err.Error()),

View file

@ -32,15 +32,6 @@ func MakeLeave(
rsAPI api.RoomserverInternalAPI, rsAPI api.RoomserverInternalAPI,
roomID, userID string, roomID, userID string,
) util.JSONResponse { ) 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) _, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil { if err != nil {
return util.JSONResponse{ return util.JSONResponse{
@ -91,7 +82,7 @@ func MakeLeave(
stateEvents[i] = &queryRes.StateEvents[i].Event stateEvents[i] = &queryRes.StateEvents[i].Event
} }
provider := gomatrixserverlib.NewAuthEvents(stateEvents) provider := gomatrixserverlib.NewAuthEvents(stateEvents)
if err = gomatrixserverlib.Allowed(*event, &provider); err != nil { if err = gomatrixserverlib.Allowed(event.Event, &provider); err != nil {
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusForbidden, Code: http.StatusForbidden,
JSON: jsonerror.Forbidden(err.Error()), JSON: jsonerror.Forbidden(err.Error()),
@ -101,7 +92,7 @@ func MakeLeave(
return util.JSONResponse{ return util.JSONResponse{
Code: http.StatusOK, Code: http.StatusOK,
JSON: map[string]interface{}{ JSON: map[string]interface{}{
"room_version": verRes.RoomVersion, "room_version": event.RoomVersion,
"event": builder, "event": builder,
}, },
} }

View file

@ -40,12 +40,12 @@ func BuildEvent(
ctx context.Context, ctx context.Context,
builder *gomatrixserverlib.EventBuilder, cfg *config.Dendrite, evTime time.Time, builder *gomatrixserverlib.EventBuilder, cfg *config.Dendrite, evTime time.Time,
rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse, rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse,
) (*gomatrixserverlib.Event, error) { ) (*gomatrixserverlib.HeaderedEvent, error) {
if queryRes == nil { if queryRes == nil {
queryRes = &api.QueryLatestEventsAndStateResponse{} queryRes = &api.QueryLatestEventsAndStateResponse{}
} }
err := AddPrevEventsToEvent(ctx, builder, rsAPI, queryRes) ver, err := AddPrevEventsToEvent(ctx, builder, rsAPI, queryRes)
if err != nil { if err != nil {
// This can pass through a ErrRoomNoExists to the caller // This can pass through a ErrRoomNoExists to the caller
return nil, err return nil, err
@ -59,7 +59,8 @@ func BuildEvent(
return nil, err 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 // AddPrevEventsToEvent fills out the prev_events and auth_events fields in builder
@ -67,14 +68,14 @@ func AddPrevEventsToEvent(
ctx context.Context, ctx context.Context,
builder *gomatrixserverlib.EventBuilder, builder *gomatrixserverlib.EventBuilder,
rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse, rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse,
) error { ) (gomatrixserverlib.RoomVersion, error) {
eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder)
if err != nil { if err != nil {
return fmt.Errorf("gomatrixserverlib.StateNeededForEventBuilder: %w", err) return "", fmt.Errorf("gomatrixserverlib.StateNeededForEventBuilder: %w", err)
} }
if len(eventsNeeded.Tuples()) == 0 { 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 // Ask the roomserver for information about this room
@ -83,16 +84,16 @@ func AddPrevEventsToEvent(
StateToFetch: eventsNeeded.Tuples(), StateToFetch: eventsNeeded.Tuples(),
} }
if err = rsAPI.QueryLatestEventsAndState(ctx, &queryReq, queryRes); err != nil { 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 { if !queryRes.RoomExists {
return ErrRoomNoExists return "", ErrRoomNoExists
} }
eventFormat, err := queryRes.RoomVersion.EventFormat() eventFormat, err := queryRes.RoomVersion.EventFormat()
if err != nil { if err != nil {
return fmt.Errorf("queryRes.RoomVersion.EventFormat: %w", err) return "", fmt.Errorf("queryRes.RoomVersion.EventFormat: %w", err)
} }
builder.Depth = queryRes.Depth builder.Depth = queryRes.Depth
@ -102,13 +103,13 @@ func AddPrevEventsToEvent(
for i := range queryRes.StateEvents { for i := range queryRes.StateEvents {
err = authEvents.AddEvent(&queryRes.StateEvents[i].Event) err = authEvents.AddEvent(&queryRes.StateEvents[i].Event)
if err != nil { if err != nil {
return fmt.Errorf("authEvents.AddEvent: %w", err) return "", fmt.Errorf("authEvents.AddEvent: %w", err)
} }
} }
refs, err := eventsNeeded.AuthEventReferences(&authEvents) refs, err := eventsNeeded.AuthEventReferences(&authEvents)
if err != nil { if err != nil {
return fmt.Errorf("eventsNeeded.AuthEventReferences: %w", err) return "", fmt.Errorf("eventsNeeded.AuthEventReferences: %w", err)
} }
truncAuth, truncPrev := truncateAuthAndPrevEvents(refs, queryRes.LatestEvents) truncAuth, truncPrev := truncateAuthAndPrevEvents(refs, queryRes.LatestEvents)
@ -128,7 +129,7 @@ func AddPrevEventsToEvent(
builder.PrevEvents = v2PrevRefs builder.PrevEvents = v2PrevRefs
} }
return nil return queryRes.RoomVersion, nil
} }
// truncateAuthAndPrevEvents limits the number of events we add into // truncateAuthAndPrevEvents limits the number of events we add into

View file

@ -18,6 +18,7 @@ import (
"context" "context"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
) )
// SendEvents to the roomserver The events are written with KindNew. // SendEvents to the roomserver The events are written with KindNew.
@ -115,3 +116,19 @@ func SendInvite(
} }
return nil 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]
}

View file

@ -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 world_readable visibility
Inbound federation can return missing events for invite visibility Inbound federation can return missing events for invite visibility
Inbound federation can get public room list 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 An event which redacts itself should be ignored
A pair of events which redact each other should be ignored A pair of events which redact each other should be ignored
Outbound federation can backfill events Outbound federation can backfill events