From 519bc1124b051273019aae9b11617ebd796e962f Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 29 Jun 2022 15:29:39 +0100 Subject: [PATCH] Add `evacuateUser` endpoint, use it when deactivating accounts (#2545) * Add `evacuateUser` endpoint, use it when deactivating accounts * Populate the API * Clean up user devices when deactivating * Include invites, delete pushers --- clientapi/routing/admin.go | 37 +++++++++++ clientapi/routing/routing.go | 6 ++ docs/administration/4_adminapi.md | 6 ++ roomserver/api/api.go | 8 +-- roomserver/api/api_trace.go | 9 +++ roomserver/api/perform.go | 9 +++ roomserver/internal/api.go | 1 + roomserver/internal/perform/perform_admin.go | 70 ++++++++++++++++++++ roomserver/inthttp/client.go | 18 +++++ roomserver/inthttp/server.go | 11 +++ userapi/internal/api.go | 26 ++++++++ userapi/userapi.go | 1 + 12 files changed, 197 insertions(+), 5 deletions(-) diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go index 125b3847d..523b88c99 100644 --- a/clientapi/routing/admin.go +++ b/clientapi/routing/admin.go @@ -47,3 +47,40 @@ func AdminEvacuateRoom(req *http.Request, device *userapi.Device, rsAPI roomserv }, } } + +func AdminEvacuateUser(req *http.Request, device *userapi.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse { + if device.AccountType != userapi.AccountTypeAdmin { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("This API can only be used by admin users."), + } + } + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + userID, ok := vars["userID"] + if !ok { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.MissingArgument("Expecting user ID."), + } + } + res := &roomserverAPI.PerformAdminEvacuateUserResponse{} + rsAPI.PerformAdminEvacuateUser( + req.Context(), + &roomserverAPI.PerformAdminEvacuateUserRequest{ + UserID: userID, + }, + res, + ) + if err := res.Error; err != nil { + return err.JSONResponse() + } + return util.JSONResponse{ + Code: 200, + JSON: map[string]interface{}{ + "affected": res.Affected, + }, + } +} diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index aa4b5a235..0460850ef 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -129,6 +129,12 @@ func Setup( }), ).Methods(http.MethodGet, http.MethodOptions) + dendriteAdminRouter.Handle("/admin/evacuateUser/{userID}", + httputil.MakeAuthAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + return AdminEvacuateUser(req, device, rsAPI) + }), + ).Methods(http.MethodGet, http.MethodOptions) + // server notifications if cfg.Matrix.ServerNotices.Enabled { logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice") diff --git a/docs/administration/4_adminapi.md b/docs/administration/4_adminapi.md index e33482ec9..51f56374b 100644 --- a/docs/administration/4_adminapi.md +++ b/docs/administration/4_adminapi.md @@ -19,6 +19,12 @@ This endpoint will instruct Dendrite to part all local users from the given `roo in the URL. It may take some time to complete. A JSON body will be returned containing the user IDs of all affected users. +## `/_dendrite/admin/evacuateUser/{userID}` + +This endpoint will instruct Dendrite to part the given local `userID` in the URL from +all rooms which they are currently joined. A JSON body will be returned containing +the room IDs of all affected rooms. + ## `/_synapse/admin/v1/register` Shared secret registration — please see the [user creation page](createusers) for diff --git a/roomserver/api/api.go b/roomserver/api/api.go index f87ff2962..38baa617f 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -140,11 +140,8 @@ type ClientRoomserverAPI interface { // PerformRoomUpgrade upgrades a room to a newer version PerformRoomUpgrade(ctx context.Context, req *PerformRoomUpgradeRequest, resp *PerformRoomUpgradeResponse) - PerformAdminEvacuateRoom( - ctx context.Context, - req *PerformAdminEvacuateRoomRequest, - res *PerformAdminEvacuateRoomResponse, - ) + PerformAdminEvacuateRoom(ctx context.Context, req *PerformAdminEvacuateRoomRequest, res *PerformAdminEvacuateRoomResponse) + PerformAdminEvacuateUser(ctx context.Context, req *PerformAdminEvacuateUserRequest, res *PerformAdminEvacuateUserResponse) PerformPeek(ctx context.Context, req *PerformPeekRequest, res *PerformPeekResponse) PerformUnpeek(ctx context.Context, req *PerformUnpeekRequest, res *PerformUnpeekResponse) PerformInvite(ctx context.Context, req *PerformInviteRequest, res *PerformInviteResponse) error @@ -161,6 +158,7 @@ type UserRoomserverAPI interface { QueryLatestEventsAndStateAPI QueryCurrentState(ctx context.Context, req *QueryCurrentStateRequest, res *QueryCurrentStateResponse) error QueryMembershipsForRoom(ctx context.Context, req *QueryMembershipsForRoomRequest, res *QueryMembershipsForRoomResponse) error + PerformAdminEvacuateUser(ctx context.Context, req *PerformAdminEvacuateUserRequest, res *PerformAdminEvacuateUserResponse) } type FederationRoomserverAPI interface { diff --git a/roomserver/api/api_trace.go b/roomserver/api/api_trace.go index 92c5c1b1d..211f320ff 100644 --- a/roomserver/api/api_trace.go +++ b/roomserver/api/api_trace.go @@ -113,6 +113,15 @@ func (t *RoomserverInternalAPITrace) PerformAdminEvacuateRoom( util.GetLogger(ctx).Infof("PerformAdminEvacuateRoom req=%+v res=%+v", js(req), js(res)) } +func (t *RoomserverInternalAPITrace) PerformAdminEvacuateUser( + ctx context.Context, + req *PerformAdminEvacuateUserRequest, + res *PerformAdminEvacuateUserResponse, +) { + t.Impl.PerformAdminEvacuateUser(ctx, req, res) + util.GetLogger(ctx).Infof("PerformAdminEvacuateUser req=%+v res=%+v", js(req), js(res)) +} + func (t *RoomserverInternalAPITrace) PerformInboundPeek( ctx context.Context, req *PerformInboundPeekRequest, diff --git a/roomserver/api/perform.go b/roomserver/api/perform.go index 30aa2cf1b..d9ea9dd1c 100644 --- a/roomserver/api/perform.go +++ b/roomserver/api/perform.go @@ -223,3 +223,12 @@ type PerformAdminEvacuateRoomResponse struct { Affected []string `json:"affected"` Error *PerformError } + +type PerformAdminEvacuateUserRequest struct { + UserID string `json:"user_id"` +} + +type PerformAdminEvacuateUserResponse struct { + Affected []string `json:"affected"` + Error *PerformError +} diff --git a/roomserver/internal/api.go b/roomserver/internal/api.go index afef52da4..acdaeef6f 100644 --- a/roomserver/internal/api.go +++ b/roomserver/internal/api.go @@ -170,6 +170,7 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio Cfg: r.Cfg, Inputer: r.Inputer, Queryer: r.Queryer, + Leaver: r.Leaver, } if err := r.Inputer.Start(); err != nil { diff --git a/roomserver/internal/perform/perform_admin.go b/roomserver/internal/perform/perform_admin.go index 2de6477cc..d3fb71099 100644 --- a/roomserver/internal/perform/perform_admin.go +++ b/roomserver/internal/perform/perform_admin.go @@ -16,6 +16,7 @@ package perform import ( "context" + "database/sql" "encoding/json" "fmt" "time" @@ -34,6 +35,7 @@ type Admin struct { Cfg *config.RoomServer Queryer *query.Queryer Inputer *input.Inputer + Leaver *Leaver } // PerformEvacuateRoom will remove all local users from the given room. @@ -160,3 +162,71 @@ func (r *Admin) PerformAdminEvacuateRoom( inputRes := &api.InputRoomEventsResponse{} r.Inputer.InputRoomEvents(ctx, inputReq, inputRes) } + +func (r *Admin) PerformAdminEvacuateUser( + ctx context.Context, + req *api.PerformAdminEvacuateUserRequest, + res *api.PerformAdminEvacuateUserResponse, +) { + _, domain, err := gomatrixserverlib.SplitID('@', req.UserID) + if err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("Malformed user ID: %s", err), + } + return + } + if domain != r.Cfg.Matrix.ServerName { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: "Can only evacuate local users using this endpoint", + } + return + } + + roomIDs, err := r.DB.GetRoomsByMembership(ctx, req.UserID, gomatrixserverlib.Join) + if err != nil && err != sql.ErrNoRows { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.DB.GetRoomsByMembership: %s", err), + } + return + } + + inviteRoomIDs, err := r.DB.GetRoomsByMembership(ctx, req.UserID, gomatrixserverlib.Invite) + if err != nil && err != sql.ErrNoRows { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.DB.GetRoomsByMembership: %s", err), + } + return + } + + for _, roomID := range append(roomIDs, inviteRoomIDs...) { + leaveReq := &api.PerformLeaveRequest{ + RoomID: roomID, + UserID: req.UserID, + } + leaveRes := &api.PerformLeaveResponse{} + outputEvents, err := r.Leaver.PerformLeave(ctx, leaveReq, leaveRes) + if err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.Leaver.PerformLeave: %s", err), + } + return + } + if len(outputEvents) == 0 { + continue + } + if err := r.Inputer.WriteOutputEvents(roomID, outputEvents); err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.Inputer.WriteOutputEvents: %s", err), + } + return + } + + res.Affected = append(res.Affected, roomID) + } +} diff --git a/roomserver/inthttp/client.go b/roomserver/inthttp/client.go index 7b10ae657..2fa8afc49 100644 --- a/roomserver/inthttp/client.go +++ b/roomserver/inthttp/client.go @@ -40,6 +40,7 @@ const ( RoomserverPerformInboundPeekPath = "/roomserver/performInboundPeek" RoomserverPerformForgetPath = "/roomserver/performForget" RoomserverPerformAdminEvacuateRoomPath = "/roomserver/performAdminEvacuateRoom" + RoomserverPerformAdminEvacuateUserPath = "/roomserver/performAdminEvacuateUser" // Query operations RoomserverQueryLatestEventsAndStatePath = "/roomserver/queryLatestEventsAndState" @@ -305,6 +306,23 @@ func (h *httpRoomserverInternalAPI) PerformAdminEvacuateRoom( } } +func (h *httpRoomserverInternalAPI) PerformAdminEvacuateUser( + ctx context.Context, + req *api.PerformAdminEvacuateUserRequest, + res *api.PerformAdminEvacuateUserResponse, +) { + span, ctx := opentracing.StartSpanFromContext(ctx, "PerformAdminEvacuateUser") + defer span.Finish() + + apiURL := h.roomserverURL + RoomserverPerformAdminEvacuateUserPath + err := httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res) + if err != nil { + res.Error = &api.PerformError{ + Msg: fmt.Sprintf("failed to communicate with roomserver: %s", err), + } + } +} + // QueryLatestEventsAndState implements RoomserverQueryAPI func (h *httpRoomserverInternalAPI) QueryLatestEventsAndState( ctx context.Context, diff --git a/roomserver/inthttp/server.go b/roomserver/inthttp/server.go index ad4fdc469..993381585 100644 --- a/roomserver/inthttp/server.go +++ b/roomserver/inthttp/server.go @@ -129,6 +129,17 @@ func AddRoutes(r api.RoomserverInternalAPI, internalAPIMux *mux.Router) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) + internalAPIMux.Handle(RoomserverPerformAdminEvacuateUserPath, + httputil.MakeInternalAPI("performAdminEvacuateUser", func(req *http.Request) util.JSONResponse { + var request api.PerformAdminEvacuateUserRequest + var response api.PerformAdminEvacuateUserResponse + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + r.PerformAdminEvacuateUser(req.Context(), &request, &response) + return util.JSONResponse{Code: http.StatusOK, JSON: &response} + }), + ) internalAPIMux.Handle( RoomserverQueryPublishedRoomsPath, httputil.MakeInternalAPI("queryPublishedRooms", func(req *http.Request) util.JSONResponse { diff --git a/userapi/internal/api.go b/userapi/internal/api.go index 9d2f63c72..27ed15a01 100644 --- a/userapi/internal/api.go +++ b/userapi/internal/api.go @@ -33,6 +33,7 @@ import ( "github.com/matrix-org/dendrite/internal/pushrules" "github.com/matrix-org/dendrite/internal/sqlutil" keyapi "github.com/matrix-org/dendrite/keyserver/api" + rsapi "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/dendrite/userapi/producers" @@ -49,6 +50,7 @@ type UserInternalAPI struct { // AppServices is the list of all registered AS AppServices []config.ApplicationService KeyAPI keyapi.UserKeyAPI + RSAPI rsapi.UserRoomserverAPI } func (a *UserInternalAPI) InputAccountData(ctx context.Context, req *api.InputAccountDataRequest, res *api.InputAccountDataResponse) error { @@ -452,6 +454,30 @@ func (a *UserInternalAPI) queryAppServiceToken(ctx context.Context, token, appSe // PerformAccountDeactivation deactivates the user's account, removing all ability for the user to login again. func (a *UserInternalAPI) PerformAccountDeactivation(ctx context.Context, req *api.PerformAccountDeactivationRequest, res *api.PerformAccountDeactivationResponse) error { + evacuateReq := &rsapi.PerformAdminEvacuateUserRequest{ + UserID: fmt.Sprintf("@%s:%s", req.Localpart, a.ServerName), + } + evacuateRes := &rsapi.PerformAdminEvacuateUserResponse{} + a.RSAPI.PerformAdminEvacuateUser(ctx, evacuateReq, evacuateRes) + if err := evacuateRes.Error; err != nil { + logrus.WithError(err).Errorf("Failed to evacuate user after account deactivation") + } + + deviceReq := &api.PerformDeviceDeletionRequest{ + UserID: fmt.Sprintf("@%s:%s", req.Localpart, a.ServerName), + } + deviceRes := &api.PerformDeviceDeletionResponse{} + if err := a.PerformDeviceDeletion(ctx, deviceReq, deviceRes); err != nil { + return err + } + + pusherReq := &api.PerformPusherDeletionRequest{ + Localpart: req.Localpart, + } + if err := a.PerformPusherDeletion(ctx, pusherReq, &struct{}{}); err != nil { + return err + } + err := a.DB.DeactivateAccount(ctx, req.Localpart) res.AccountDeactivated = err == nil return err diff --git a/userapi/userapi.go b/userapi/userapi.go index 603b416bf..2e86d6aa7 100644 --- a/userapi/userapi.go +++ b/userapi/userapi.go @@ -78,6 +78,7 @@ func NewInternalAPI( ServerName: cfg.Matrix.ServerName, AppServices: appServices, KeyAPI: keyAPI, + RSAPI: rsAPI, DisableTLSValidation: cfg.PushGatewayDisableTLSValidation, }