From 6c43fe52a04705d795015d8b774153323a289bee Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 12 Aug 2022 10:02:49 +0100 Subject: [PATCH] Use HTTP API to reset password, add option to User API `PerformPasswordUpdate` to invalidate sessions --- clientapi/routing/admin.go | 72 +++++++++++++++++++++++------ clientapi/routing/routing.go | 14 ++++-- cmd/create-account/main.go | 88 ++++++++++++++++++------------------ internal/httputil/httpapi.go | 18 ++++++++ userapi/api/api.go | 5 +- userapi/internal/api.go | 5 ++ 6 files changed, 138 insertions(+), 64 deletions(-) diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go index a8dd0e64f..1cf0fb265 100644 --- a/clientapi/routing/admin.go +++ b/clientapi/routing/admin.go @@ -1,23 +1,20 @@ package routing import ( + "encoding/json" "net/http" "github.com/gorilla/mux" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/internal/httputil" 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/gomatrixserverlib" "github.com/matrix-org/util" ) -func AdminEvacuateRoom(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."), - } - } +func AdminEvacuateRoom(req *http.Request, cfg *config.ClientAPI, device *userapi.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -50,13 +47,7 @@ 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."), - } - } +func AdminEvacuateUser(req *http.Request, cfg *config.ClientAPI, device *userapi.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -68,6 +59,16 @@ func AdminEvacuateUser(req *http.Request, device *userapi.Device, rsAPI roomserv JSON: jsonerror.MissingArgument("Expecting user ID."), } } + _, domain, err := gomatrixserverlib.SplitID('@', userID) + if err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + if domain != cfg.Matrix.ServerName { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.MissingArgument("User ID must belong to this server."), + } + } res := &roomserverAPI.PerformAdminEvacuateUserResponse{} if err := rsAPI.PerformAdminEvacuateUser( req.Context(), @@ -88,3 +89,46 @@ func AdminEvacuateUser(req *http.Request, device *userapi.Device, rsAPI roomserv }, } } + +func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + localpart, ok := vars["localpart"] + if !ok { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.MissingArgument("Expecting user localpart."), + } + } + request := struct { + Password string `json:"password"` + }{} + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + if request.Password == "" { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.MissingArgument("Expecting non-empty password."), + } + } + updateReq := &userapi.PerformPasswordUpdateRequest{ + Localpart: localpart, + Password: request.Password, + LogoutDevices: true, + } + updateRes := &userapi.PerformPasswordUpdateResponse{} + if err := userAPI.PerformPasswordUpdate(req.Context(), updateReq, updateRes); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + return util.JSONResponse{ + Code: http.StatusOK, + JSON: struct { + Updated bool `json:"password_updated"` + }{ + Updated: updateRes.PasswordUpdated, + }, + } +} diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index ced4fdbcf..d8bb2a8f0 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -144,17 +144,23 @@ func Setup( } dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}", - httputil.MakeAuthAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { - return AdminEvacuateRoom(req, device, rsAPI) + httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + return AdminEvacuateRoom(req, cfg, device, rsAPI) }), ).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) + httputil.MakeAdminAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + return AdminEvacuateUser(req, cfg, device, rsAPI) }), ).Methods(http.MethodGet, http.MethodOptions) + dendriteAdminRouter.Handle("/admin/resetPassword/{userID}", + httputil.MakeAdminAPI("admin_reset_password", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + return AdminResetPassword(req, cfg, device, userAPI) + }), + ).Methods(http.MethodPost, http.MethodOptions) + // server notifications if cfg.Matrix.ServerNotices.Enabled { logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice") diff --git a/cmd/create-account/main.go b/cmd/create-account/main.go index 2340f2dac..6b7c79c1d 100644 --- a/cmd/create-account/main.go +++ b/cmd/create-account/main.go @@ -16,7 +16,6 @@ package main import ( "bytes" - "context" "crypto/hmac" "crypto/sha1" "encoding/hex" @@ -24,6 +23,7 @@ import ( "flag" "fmt" "io" + "net/http" "os" "regexp" "strings" @@ -35,8 +35,6 @@ import ( "golang.org/x/term" "github.com/matrix-org/dendrite/setup" - "github.com/matrix-org/dendrite/setup/base" - "github.com/matrix-org/dendrite/userapi/storage" ) const usage = `Usage: %s @@ -72,6 +70,11 @@ var ( validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-=./]+$`) ) +var cl = http.Client{ + Timeout: time.Second * 10, + Transport: http.DefaultTransport, +} + func main() { name := os.Args[0] flag.Usage = func() { @@ -100,40 +103,9 @@ func main() { } if *resetPassword { - var ( - accountDB storage.Database - available bool - ) - b := base.NewBaseDendrite(cfg, "") - defer b.Close() // nolint: errcheck - accountDB, err = storage.NewUserAPIDatabase( - b, - &cfg.UserAPI.AccountDatabase, - cfg.Global.ServerName, - cfg.UserAPI.BCryptCost, - cfg.UserAPI.OpenIDTokenLifetimeMS, - 0, // TODO - cfg.Global.ServerNotices.LocalPart, - ) - if err != nil { - logrus.WithError(err).Fatalln("Failed to connect to the database") + if err = passwordReset(*serverURL, *username, pass); err != nil { + logrus.Fatalln("Failed to reset the password:", err.Error()) } - - available, err = accountDB.CheckAccountAvailability(context.Background(), *username) - if err != nil { - logrus.Fatalln("Unable check username existence.") - } - if available { - logrus.Fatalln("Username could not be found.") - } - err = accountDB.SetPassword(context.Background(), *username, pass) - if err != nil { - logrus.Fatalf("Failed to update password for user %s: %s", *username, err.Error()) - } - if _, err = accountDB.RemoveAllDevices(context.Background(), *username, ""); err != nil { - logrus.Fatalf("Failed to remove all devices: %s", err.Error()) - } - logrus.Infof("Updated password for user %s and invalidated all logins\n", *username) return } @@ -145,6 +117,39 @@ func main() { logrus.Infof("Created account: %s (AccessToken: %s)", *username, accessToken) } +func passwordReset(serverURL, localpart, password string) error { + resetURL := fmt.Sprintf("%s/_dendrite/admin/resetPassword/%s", serverURL, localpart) + request := struct { + Password string `json:"password"` + }{ + Password: password, + } + response := struct { + Updated bool `json:"password_updated"` + }{} + js, err := json.Marshal(request) + if err != nil { + return fmt.Errorf("unable to marshal json: %w", err) + } + registerReq, err := http.NewRequest(http.MethodPost, resetURL, bytes.NewBuffer(js)) + if err != nil { + return fmt.Errorf("unable to create http request: %w", err) + } + httpResp, err := cl.Do(registerReq) + if err != nil { + return fmt.Errorf("unable to create account: %w", err) + } + if err := json.NewDecoder(httpResp.Body).Decode(&response); err != nil { + return fmt.Errorf("unable to decode response: %w", err) + } + if response.Updated { + logrus.Infof("Reset password for user %q and invalidated all user sessions", localpart) + } else { + logrus.Infof("Failed to reset password for user %q", localpart) + } + return nil +} + type sharedSecretRegistrationRequest struct { User string `json:"username"` Password string `json:"password"` @@ -155,20 +160,15 @@ type sharedSecretRegistrationRequest struct { func sharedSecretRegister(sharedSecret, serverURL, localpart, password string, admin bool) (accesToken string, err error) { registerURL := fmt.Sprintf("%s/_synapse/admin/v1/register", serverURL) - cl := http.Client{ - Timeout: time.Second * 10, - Transport: http.DefaultTransport, - } nonceReq, err := http.NewRequest(http.MethodGet, registerURL, nil) if err != nil { return "", fmt.Errorf("unable to create http request: %w", err) } - nonceResp, err := cl.Do(nonceReq) if err != nil { return "", fmt.Errorf("unable to get nonce: %w", err) } - body, err := ioutil.ReadAll(nonceResp.Body) + body, err := io.ReadAll(nonceResp.Body) if err != nil { return "", fmt.Errorf("failed to read response body: %w", err) } @@ -207,10 +207,10 @@ func sharedSecretRegister(sharedSecret, serverURL, localpart, password string, a } defer regResp.Body.Close() // nolint: errcheck if regResp.StatusCode < 200 || regResp.StatusCode >= 300 { - body, _ = ioutil.ReadAll(regResp.Body) + body, _ = io.ReadAll(regResp.Body) return "", fmt.Errorf(gjson.GetBytes(body, "error").Str) } - r, _ := ioutil.ReadAll(regResp.Body) + r, _ := io.ReadAll(regResp.Body) return gjson.GetBytes(r, "access_token").Str, nil } diff --git a/internal/httputil/httpapi.go b/internal/httputil/httpapi.go index aba50ae4d..e0436c60a 100644 --- a/internal/httputil/httpapi.go +++ b/internal/httputil/httpapi.go @@ -25,6 +25,7 @@ import ( "github.com/getsentry/sentry-go" "github.com/matrix-org/dendrite/clientapi/auth" + "github.com/matrix-org/dendrite/clientapi/jsonerror" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/util" opentracing "github.com/opentracing/opentracing-go" @@ -83,6 +84,23 @@ func MakeAuthAPI( return MakeExternalAPI(metricsName, h) } +// MakeAdminAPI is a wrapper around MakeAuthAPI which enforces that the request can only be +// completed by a user that is a server administrator. +func MakeAdminAPI( + metricsName string, userAPI userapi.QueryAcccessTokenAPI, + f func(*http.Request, *userapi.Device) util.JSONResponse, +) http.Handler { + return MakeAuthAPI(metricsName, userAPI, func(req *http.Request, device *userapi.Device) 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."), + } + } + return f(req, device) + }) +} + // MakeExternalAPI turns a util.JSONRequestHandler function into an http.Handler. // This is used for APIs that are called from the internet. func MakeExternalAPI(metricsName string, f func(*http.Request) util.JSONResponse) http.Handler { diff --git a/userapi/api/api.go b/userapi/api/api.go index 388f97cb4..66ee9c7c8 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -334,8 +334,9 @@ type PerformAccountCreationResponse struct { // PerformAccountCreationRequest is the request for PerformAccountCreation type PerformPasswordUpdateRequest struct { - Localpart string // Required: The localpart for this account. - Password string // Required: The new password to set. + Localpart string // Required: The localpart for this account. + Password string // Required: The new password to set. + LogoutDevices bool // Optional: Whether to log out all user devices. } // PerformAccountCreationResponse is the response for PerformAccountCreation diff --git a/userapi/internal/api.go b/userapi/internal/api.go index 78b226d46..6ba469327 100644 --- a/userapi/internal/api.go +++ b/userapi/internal/api.go @@ -139,6 +139,11 @@ func (a *UserInternalAPI) PerformPasswordUpdate(ctx context.Context, req *api.Pe if err := a.DB.SetPassword(ctx, req.Localpart, req.Password); err != nil { return err } + if req.LogoutDevices { + if _, err := a.DB.RemoveAllDevices(context.Background(), req.Localpart, ""); err != nil { + return err + } + } res.PasswordUpdated = true return nil }