mirror of
https://github.com/matrix-org/dendrite.git
synced 2024-11-23 23:01:56 -06:00
432c35a307
* Allow user to forget a room, even if they never were a member * Return "M_UNKNOWN" as per the spec Co-authored-by: kegsay <kegan@matrix.org>
481 lines
15 KiB
Go
481 lines
15 KiB
Go
// Copyright 2017 Vector Creations Ltd
|
|
//
|
|
// 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"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
|
"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/threepid"
|
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
|
"github.com/matrix-org/dendrite/setup/config"
|
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
|
"github.com/matrix-org/gomatrixserverlib"
|
|
|
|
"github.com/matrix-org/util"
|
|
)
|
|
|
|
var errMissingUserID = errors.New("'user_id' must be supplied")
|
|
|
|
func SendBan(
|
|
req *http.Request, accountDB accounts.Database, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
|
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
|
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
|
EventType: gomatrixserverlib.MRoomPowerLevels,
|
|
StateKey: "",
|
|
})
|
|
if plEvent == nil {
|
|
return util.JSONResponse{
|
|
Code: 403,
|
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, 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 ban this user, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
|
}
|
|
}
|
|
allowedToBan := pl.UserLevel(device.UserID) >= pl.Ban
|
|
if !allowedToBan {
|
|
return util.JSONResponse{
|
|
Code: 403,
|
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, power level too low."),
|
|
}
|
|
}
|
|
|
|
return sendMembership(req.Context(), accountDB, device, roomID, "ban", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
|
}
|
|
|
|
func sendMembership(ctx context.Context, accountDB accounts.Database, device *userapi.Device,
|
|
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
|
roomVer gomatrixserverlib.RoomVersion,
|
|
rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI) util.JSONResponse {
|
|
|
|
event, err := buildMembershipEvent(
|
|
ctx, targetUserID, reason, accountDB, device, membership,
|
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
|
)
|
|
if err == errMissingUserID {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(err.Error()),
|
|
}
|
|
} else if err == eventutil.ErrRoomNoExists {
|
|
return util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound(err.Error()),
|
|
}
|
|
} else if err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
if err = roomserverAPI.SendEvents(
|
|
ctx, rsAPI,
|
|
roomserverAPI.KindNew,
|
|
[]*gomatrixserverlib.HeaderedEvent{event.Event.Headered(roomVer)},
|
|
cfg.Matrix.ServerName,
|
|
cfg.Matrix.ServerName,
|
|
nil,
|
|
false,
|
|
); err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}
|
|
}
|
|
|
|
func SendKick(
|
|
req *http.Request, accountDB accounts.Database, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
if body.UserID == "" {
|
|
return util.JSONResponse{
|
|
Code: 400,
|
|
JSON: jsonerror.BadJSON("missing user_id"),
|
|
}
|
|
}
|
|
|
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
|
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
|
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
|
RoomID: roomID,
|
|
UserID: body.UserID,
|
|
}, &queryRes)
|
|
if err != nil {
|
|
return util.ErrorResponse(err)
|
|
}
|
|
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
|
if queryRes.Membership != "join" && queryRes.Membership != "invite" {
|
|
return util.JSONResponse{
|
|
Code: 403,
|
|
JSON: jsonerror.Unknown("cannot /kick banned or left users"),
|
|
}
|
|
}
|
|
// TODO: should we be using SendLeave instead?
|
|
return sendMembership(req.Context(), accountDB, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
|
}
|
|
|
|
func SendUnban(
|
|
req *http.Request, accountDB accounts.Database, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
if body.UserID == "" {
|
|
return util.JSONResponse{
|
|
Code: 400,
|
|
JSON: jsonerror.BadJSON("missing user_id"),
|
|
}
|
|
}
|
|
|
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
|
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
|
RoomID: roomID,
|
|
UserID: body.UserID,
|
|
}, &queryRes)
|
|
if err != nil {
|
|
return util.ErrorResponse(err)
|
|
}
|
|
// unban is only valid if the user is currently banned
|
|
if queryRes.Membership != "ban" {
|
|
return util.JSONResponse{
|
|
Code: 400,
|
|
JSON: jsonerror.Unknown("can only /unban users that are banned"),
|
|
}
|
|
}
|
|
// TODO: should we be using SendLeave instead?
|
|
return sendMembership(req.Context(), accountDB, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
|
}
|
|
|
|
func SendInvite(
|
|
req *http.Request, accountDB accounts.Database, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, _, reqErr := extractRequestData(req, roomID, rsAPI)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
|
|
inviteStored, jsonErrResp := checkAndProcessThreepid(
|
|
req, device, body, cfg, rsAPI, accountDB, roomID, evTime,
|
|
)
|
|
if jsonErrResp != nil {
|
|
return *jsonErrResp
|
|
}
|
|
|
|
// If an invite has been stored on an identity server, it means that a
|
|
// m.room.third_party_invite event has been emitted and that we shouldn't
|
|
// emit a m.room.member one.
|
|
if inviteStored {
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}
|
|
}
|
|
|
|
event, err := buildMembershipEvent(
|
|
req.Context(), body.UserID, body.Reason, accountDB, device, "invite",
|
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
|
)
|
|
if err == errMissingUserID {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(err.Error()),
|
|
}
|
|
} else if err == eventutil.ErrRoomNoExists {
|
|
return util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound(err.Error()),
|
|
}
|
|
} else if err != nil {
|
|
util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvent failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
err = roomserverAPI.SendInvite(
|
|
req.Context(), rsAPI,
|
|
event,
|
|
nil, // ask the roomserver to draw up invite room state for us
|
|
cfg.Matrix.ServerName,
|
|
nil,
|
|
)
|
|
switch e := err.(type) {
|
|
case *roomserverAPI.PerformError:
|
|
return e.JSONResponse()
|
|
case nil:
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}
|
|
default:
|
|
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInvite failed")
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: jsonerror.InternalServerError(),
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildMembershipEvent(
|
|
ctx context.Context,
|
|
targetUserID, reason string, accountDB accounts.Database,
|
|
device *userapi.Device,
|
|
membership, roomID string, isDirect bool,
|
|
cfg *config.ClientAPI, evTime time.Time,
|
|
rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI,
|
|
) (*gomatrixserverlib.HeaderedEvent, error) {
|
|
profile, err := loadProfile(ctx, targetUserID, cfg, accountDB, asAPI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
builder := gomatrixserverlib.EventBuilder{
|
|
Sender: device.UserID,
|
|
RoomID: roomID,
|
|
Type: "m.room.member",
|
|
StateKey: &targetUserID,
|
|
}
|
|
|
|
content := gomatrixserverlib.MemberContent{
|
|
Membership: membership,
|
|
DisplayName: profile.DisplayName,
|
|
AvatarURL: profile.AvatarURL,
|
|
Reason: reason,
|
|
IsDirect: isDirect,
|
|
}
|
|
|
|
if err = builder.SetContent(content); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return eventutil.QueryAndBuildEvent(ctx, &builder, cfg.Matrix, evTime, rsAPI, nil)
|
|
}
|
|
|
|
// loadProfile lookups the profile of a given user from the database and returns
|
|
// it if the user is local to this server, or returns an empty profile if not.
|
|
// Returns an error if the retrieval failed or if the first parameter isn't a
|
|
// valid Matrix ID.
|
|
func loadProfile(
|
|
ctx context.Context,
|
|
userID string,
|
|
cfg *config.ClientAPI,
|
|
accountDB accounts.Database,
|
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
|
) (*authtypes.Profile, error) {
|
|
_, serverName, err := gomatrixserverlib.SplitID('@', userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var profile *authtypes.Profile
|
|
if serverName == cfg.Matrix.ServerName {
|
|
profile, err = appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, accountDB)
|
|
} else {
|
|
profile = &authtypes.Profile{}
|
|
}
|
|
|
|
return profile, err
|
|
}
|
|
|
|
func extractRequestData(req *http.Request, roomID string, rsAPI roomserverAPI.RoomserverInternalAPI) (
|
|
body *threepid.MembershipRequest, evTime time.Time, roomVer gomatrixserverlib.RoomVersion, resErr *util.JSONResponse,
|
|
) {
|
|
verReq := roomserverAPI.QueryRoomVersionForRoomRequest{RoomID: roomID}
|
|
verRes := roomserverAPI.QueryRoomVersionForRoomResponse{}
|
|
if err := rsAPI.QueryRoomVersionForRoom(req.Context(), &verReq, &verRes); err != nil {
|
|
resErr = &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.UnsupportedRoomVersion(err.Error()),
|
|
}
|
|
return
|
|
}
|
|
roomVer = verRes.RoomVersion
|
|
|
|
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
|
resErr = reqErr
|
|
return
|
|
}
|
|
|
|
evTime, err := httputil.ParseTSParam(req)
|
|
if err != nil {
|
|
resErr = &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
|
}
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func checkAndProcessThreepid(
|
|
req *http.Request,
|
|
device *userapi.Device,
|
|
body *threepid.MembershipRequest,
|
|
cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
|
accountDB accounts.Database,
|
|
roomID string,
|
|
evTime time.Time,
|
|
) (inviteStored bool, errRes *util.JSONResponse) {
|
|
|
|
inviteStored, err := threepid.CheckAndProcessInvite(
|
|
req.Context(), device, body, cfg, rsAPI, accountDB,
|
|
roomID, evTime,
|
|
)
|
|
if err == threepid.ErrMissingParameter {
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(err.Error()),
|
|
}
|
|
} else if err == threepid.ErrNotTrusted {
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.NotTrusted(body.IDServer),
|
|
}
|
|
} else if err == eventutil.ErrRoomNoExists {
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound(err.Error()),
|
|
}
|
|
} else if e, ok := err.(gomatrixserverlib.BadJSONError); ok {
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(e.Error()),
|
|
}
|
|
}
|
|
if err != nil {
|
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
er := jsonerror.InternalServerError()
|
|
return inviteStored, &er
|
|
}
|
|
return
|
|
}
|
|
|
|
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.RoomserverInternalAPI, userID, roomID string) *util.JSONResponse {
|
|
tuple := gomatrixserverlib.StateKeyTuple{
|
|
EventType: gomatrixserverlib.MRoomMember,
|
|
StateKey: userID,
|
|
}
|
|
var membershipRes roomserverAPI.QueryCurrentStateResponse
|
|
err := rsAPI.QueryCurrentState(ctx, &roomserverAPI.QueryCurrentStateRequest{
|
|
RoomID: roomID,
|
|
StateTuples: []gomatrixserverlib.StateKeyTuple{tuple},
|
|
}, &membershipRes)
|
|
if err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("QueryCurrentState: could not query membership for user")
|
|
e := jsonerror.InternalServerError()
|
|
return &e
|
|
}
|
|
ev := membershipRes.StateEvents[tuple]
|
|
if ev == nil {
|
|
return &util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: jsonerror.Forbidden("user does not belong to room"),
|
|
}
|
|
}
|
|
membership, err := ev.Membership()
|
|
if err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("Member event isn't valid")
|
|
e := jsonerror.InternalServerError()
|
|
return &e
|
|
}
|
|
if membership != gomatrixserverlib.Join {
|
|
return &util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: jsonerror.Forbidden("user does not belong to room"),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func SendForget(
|
|
req *http.Request, device *userapi.Device,
|
|
roomID string, rsAPI roomserverAPI.RoomserverInternalAPI,
|
|
) util.JSONResponse {
|
|
ctx := req.Context()
|
|
logger := util.GetLogger(ctx).WithField("roomID", roomID).WithField("userID", device.UserID)
|
|
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
|
membershipReq := roomserverAPI.QueryMembershipForUserRequest{
|
|
RoomID: roomID,
|
|
UserID: device.UserID,
|
|
}
|
|
err := rsAPI.QueryMembershipForUser(ctx, &membershipReq, &membershipRes)
|
|
if err != nil {
|
|
logger.WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
if membershipRes.IsInRoom {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.Unknown(fmt.Sprintf("User %s is in room %s", device.UserID, roomID)),
|
|
}
|
|
}
|
|
|
|
request := roomserverAPI.PerformForgetRequest{
|
|
RoomID: roomID,
|
|
UserID: device.UserID,
|
|
}
|
|
response := roomserverAPI.PerformForgetResponse{}
|
|
if err := rsAPI.PerformForget(ctx, &request, &response); err != nil {
|
|
logger.WithError(err).Error("PerformForget: unable to forget room")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}
|
|
}
|