Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement event redaction #768

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
180 changes: 180 additions & 0 deletions clientapi/routing/redact.go
@@ -0,0 +1,180 @@
// Copyright 2019 Alex Chen
//
// 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/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/common/transactions"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)

// https://matrix.org/docs/spec/client_server/r0.5.0#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid

type redactRequest struct {
Reason string `json:"reason,omitempty"`
}

type redactResponse struct {
EventID string `json:"event_id"`
}

// Redact implements PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}
// and POST /_matrix/client/r0/rooms/{roomId}/redact/{eventId} (mainly for SyTest)
func Redact(
req *http.Request,
device *authtypes.Device,
roomID, redactedEventID string, txnID *string,
cfg config.Dendrite,
queryAPI api.RoomserverQueryAPI,
producer *producers.RoomserverProducer,
txnCache *transactions.Cache,
) util.JSONResponse {
// TODO: Idempotency

var redactReq redactRequest
if resErr := httputil.UnmarshalJSONRequest(req, &redactReq); resErr != nil {
return *resErr
}

// Build a redaction event
builder := gomatrixserverlib.EventBuilder{
Sender: device.UserID,
RoomID: roomID,
Redacts: redactedEventID,
Type: gomatrixserverlib.MRoomRedaction,
}
err := builder.SetContent(redactReq)
if err != nil {
return httputil.LogThenError(req, err)
}

var queryRes api.QueryLatestEventsAndStateResponse
e, err := common.BuildEvent(req.Context(), &builder, cfg, time.Now(), queryAPI, &queryRes)
if err == common.ErrRoomNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Room does not exist"),
}
} else if err != nil {
return httputil.LogThenError(req, err)
}

if resErr := checkRedactionAllowed(
req, queryAPI, e, queryRes.StateEvents,
); resErr != nil {
return *resErr
}

// Send the redaction event

var txnAndDeviceID *api.TransactionID
if txnID != nil {
txnAndDeviceID = &api.TransactionID{
TransactionID: *txnID,
DeviceID: device.ID,
}
}

// pass the new event to the roomserver and receive the correct event ID
// event ID in case of duplicate transaction is discarded
eventID, err := producer.SendEvents(
req.Context(), []gomatrixserverlib.Event{*e}, cfg.Matrix.ServerName, txnAndDeviceID,
)
if err != nil {
return httputil.LogThenError(req, err)
}

return util.JSONResponse{
Code: http.StatusOK,
JSON: redactResponse{eventID},
}
}

func checkRedactionAllowed(
req *http.Request, queryAPI api.RoomserverQueryAPI,
redactionEvent *gomatrixserverlib.Event,
stateEvents []gomatrixserverlib.Event,
) *util.JSONResponse {
// Do some basic checks e.g. ensuring the user is in the room and can send m.room.redaction events
statEventPointers := make([]*gomatrixserverlib.Event, len(stateEvents))
for i := range stateEvents {
statEventPointers[i] = &stateEvents[i]
}
provider := gomatrixserverlib.NewAuthEvents(statEventPointers)
if err := gomatrixserverlib.Allowed(*redactionEvent, &provider); err != nil {
// TODO: Is the error returned with suitable HTTP status code?
if _, ok := err.(*gomatrixserverlib.NotAllowed); ok {
return &util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden(err.Error()),
}
}

res := httputil.LogThenError(req, err)
return &res
}

// Ensure the user can redact the specific event

eventReq := api.QueryEventsByIDRequest{
EventIDs: []string{redactionEvent.Redacts()},
}
var eventResp api.QueryEventsByIDResponse
if err := queryAPI.QueryEventsByID(req.Context(), &eventReq, &eventResp); err != nil {
res := httputil.LogThenError(req, err)
return &res
}

if len(eventResp.Events) == 0 {
return &util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Event to redact not found"),
}
}

redactedEvent := eventResp.Events[0]

badEvents, _, err := common.ValidateRedaction(&redactedEvent, redactionEvent)
if err != nil {
res := httputil.LogThenError(req, err)
return &res
}
if badEvents {
return &util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("invalid redaction attempt"),
}
}

if redactedEvent.Sender() != redactionEvent.Sender() {
// TODO: Allow power users to redact others' events
return &util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("You are not allowed to redact this event"),
}
}

return nil
}
22 changes: 22 additions & 0 deletions clientapi/routing/routing.go
Expand Up @@ -169,6 +169,28 @@ func Setup(
}),
).Methods(http.MethodPut, http.MethodOptions)

r0mux.Handle("/rooms/{roomID}/redact/{eventID}/{txnID}",
common.MakeAuthAPI("redact", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars, err := common.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
txnID := vars["txnID"]
return Redact(req, device, vars["roomID"], vars["eventID"], &txnID,
cfg, queryAPI, producer, transactionsCache)
}),
).Methods(http.MethodPut, http.MethodOptions)
r0mux.Handle("/rooms/{roomID}/redact/{eventID}",
common.MakeAuthAPI("redact", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars, err := common.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
return Redact(req, device, vars["roomID"], vars["eventID"], nil,
cfg, queryAPI, producer, transactionsCache)
}),
).Methods(http.MethodPost, http.MethodOptions)

r0mux.Handle("/register", common.MakeExternalAPI("register", func(req *http.Request) util.JSONResponse {
return Register(req, accountDB, deviceDB, &cfg)
})).Methods(http.MethodPost, http.MethodOptions)
Expand Down
44 changes: 44 additions & 0 deletions common/redaction.go
@@ -0,0 +1,44 @@
// Copyright 2019 Alex Chen
//
// 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 common

import "github.com/matrix-org/gomatrixserverlib"

func ValidateRedaction(
redacted, redaction *gomatrixserverlib.Event,
) (badEvents, needPowerLevelCheck bool, err error) {
// Don't allow redaction of events in different rooms
if redaction.RoomID() != redacted.RoomID() {
return true, false, nil
}

var expectedDomain, redactorDomain gomatrixserverlib.ServerName
if _, expectedDomain, err = gomatrixserverlib.SplitID(
'@', redacted.Sender(),
); err != nil {
return false, false, err
}
if _, redactorDomain, err = gomatrixserverlib.SplitID(
'@', redaction.Sender(),
); err != nil {
return false, false, err
}

if expectedDomain != redactorDomain {
return false, true, err
}

return false, false, nil
}
6 changes: 4 additions & 2 deletions roomserver/storage/event_json_table.go
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"database/sql"

"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/roomserver/types"
)

Expand Down Expand Up @@ -67,9 +68,10 @@ func (s *eventJSONStatements) prepare(db *sql.DB) (err error) {
}

func (s *eventJSONStatements) insertEventJSON(
ctx context.Context, eventNID types.EventNID, eventJSON []byte,
ctx context.Context, txn *sql.Tx, eventNID types.EventNID, eventJSON []byte,
) error {
_, err := s.insertEventJSONStmt.ExecContext(ctx, int64(eventNID), eventJSON)
stmt := common.TxStmt(txn, s.insertEventJSONStmt)
_, err := stmt.ExecContext(ctx, int64(eventNID), eventJSON)
return err
}

Expand Down
10 changes: 6 additions & 4 deletions roomserver/storage/events_table.go
Expand Up @@ -155,7 +155,7 @@ func (s *eventStatements) prepare(db *sql.DB) (err error) {
}

func (s *eventStatements) insertEvent(
ctx context.Context,
ctx context.Context, txn *sql.Tx,
roomNID types.RoomNID,
eventTypeNID types.EventTypeNID,
eventStateKeyNID types.EventStateKeyNID,
Expand All @@ -166,19 +166,21 @@ func (s *eventStatements) insertEvent(
) (types.EventNID, types.StateSnapshotNID, error) {
var eventNID int64
var stateNID int64
err := s.insertEventStmt.QueryRowContext(
stmt := common.TxStmt(txn, s.insertEventStmt)
err := stmt.QueryRowContext(
ctx, int64(roomNID), int64(eventTypeNID), int64(eventStateKeyNID),
eventID, referenceSHA256, eventNIDsAsArray(authEventNIDs), depth,
).Scan(&eventNID, &stateNID)
return types.EventNID(eventNID), types.StateSnapshotNID(stateNID), err
}

func (s *eventStatements) selectEvent(
ctx context.Context, eventID string,
ctx context.Context, txn *sql.Tx, eventID string,
) (types.EventNID, types.StateSnapshotNID, error) {
var eventNID int64
var stateNID int64
err := s.selectEventStmt.QueryRowContext(ctx, eventID).Scan(&eventNID, &stateNID)
stmt := common.TxStmt(txn, s.selectEventStmt)
err := stmt.QueryRowContext(ctx, eventID).Scan(&eventNID, &stateNID)
return types.EventNID(eventNID), types.StateSnapshotNID(stateNID), err
}

Expand Down