aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTill <2353100+S7evinK@users.noreply.github.com>2022-08-12 13:00:07 +0200
committerGitHub <noreply@github.com>2022-08-12 12:00:07 +0100
commit48600d554096d54d12a6d3834f8f5f45506e3026 (patch)
tree75623c455d3294b77e140f2a306ab29198089429
parentfad3ac8e787cebd231d6fe115980f6335bc94353 (diff)
Use `/admin/v1/register` in `create-account` (#2484)
* Get all account data on CompleteSync * Revert "Get all account data on CompleteSync" This reverts commit 44a3e566d8fb940b0b757aea9b8408fa19ea9f54. * Use /_synapse/admin/v1/register to create account * Linting * Linter again :) * Update docs * Use HTTP API to reset password, add option to User API `PerformPasswordUpdate` to invalidate sessions * Fix routing name * Tell me more about what went wrong * Deprecate the `-reset-password` flag, document the new API Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
-rw-r--r--clientapi/routing/admin.go78
-rw-r--r--clientapi/routing/routing.go14
-rw-r--r--cmd/create-account/main.go146
-rw-r--r--docs/administration/1_createusers.md12
-rw-r--r--docs/administration/4_adminapi.md19
-rw-r--r--internal/httputil/httpapi.go18
-rw-r--r--userapi/api/api.go5
-rw-r--r--userapi/internal/api.go5
8 files changed, 217 insertions, 80 deletions
diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go
index a8dd0e64..0c5f8c16 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,52 @@ 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.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.Unknown("Failed to decode request body: " + 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.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.Unknown("Failed to perform password update: " + 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 ced4fdbc..2063a008 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/{localpart}",
+ 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 92179a04..44d5691c 100644
--- a/cmd/create-account/main.go
+++ b/cmd/create-account/main.go
@@ -15,20 +15,26 @@
package main
import (
- "context"
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/hex"
+ "encoding/json"
"flag"
"fmt"
"io"
+ "net/http"
"os"
"regexp"
"strings"
+ "time"
+
+ "github.com/tidwall/gjson"
- "github.com/matrix-org/dendrite/setup"
- "github.com/matrix-org/dendrite/setup/base"
- "github.com/matrix-org/dendrite/userapi/api"
- "github.com/matrix-org/dendrite/userapi/storage"
"github.com/sirupsen/logrus"
"golang.org/x/term"
+
+ "github.com/matrix-org/dendrite/setup"
)
const usage = `Usage: %s
@@ -58,12 +64,17 @@ var (
password = flag.String("password", "", "The password to associate with the account")
pwdFile = flag.String("passwordfile", "", "The file to use for the password (e.g. for automated account creation)")
pwdStdin = flag.Bool("passwordstdin", false, "Reads the password from stdin")
- pwdLess = flag.Bool("passwordless", false, "Create a passwordless account, e.g. if only an accesstoken is required")
isAdmin = flag.Bool("admin", false, "Create an admin account")
resetPassword = flag.Bool("reset-password", false, "Resets the password for the given username")
+ serverURL = flag.String("url", "https://localhost:8448", "The URL to connect to.")
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() {
@@ -72,15 +83,15 @@ func main() {
}
cfg := setup.ParseFlags(true)
+ if *resetPassword {
+ logrus.Fatalf("The reset-password flag has been replaced by the POST /_dendrite/admin/resetPassword/{localpart} admin API.")
+ }
+
if *username == "" {
flag.Usage()
os.Exit(1)
}
- if *pwdLess && *resetPassword {
- logrus.Fatalf("Can not reset to an empty password, unable to login afterwards.")
- }
-
if !validUsernameRegex.MatchString(*username) {
logrus.Warn("Username can only contain characters a-z, 0-9, or '_-./='")
os.Exit(1)
@@ -90,67 +101,94 @@ func main() {
logrus.Fatalf("Username can not be longer than 255 characters: %s", fmt.Sprintf("@%s:%s", *username, cfg.Global.ServerName))
}
- var pass string
- var err error
- if !*pwdLess {
- pass, err = getPassword(*password, *pwdFile, *pwdStdin, os.Stdin)
- if err != nil {
- logrus.Fatalln(err)
- }
+ pass, err := getPassword(*password, *pwdFile, *pwdStdin, os.Stdin)
+ if err != nil {
+ logrus.Fatalln(err)
+ }
+
+ accessToken, err := sharedSecretRegister(cfg.ClientAPI.RegistrationSharedSecret, *serverURL, *username, pass, *isAdmin)
+ if err != nil {
+ logrus.Fatalln("Failed to create the account:", err.Error())
}
- // avoid warning about open registration
- cfg.ClientAPI.RegistrationDisabled = true
+ logrus.Infof("Created account: %s (AccessToken: %s)", *username, accessToken)
+}
- b := base.NewBaseDendrite(cfg, "")
- defer b.Close() // nolint: errcheck
+type sharedSecretRegistrationRequest struct {
+ User string `json:"username"`
+ Password string `json:"password"`
+ Nonce string `json:"nonce"`
+ MacStr string `json:"mac"`
+ Admin bool `json:"admin"`
+}
- accountDB, err := storage.NewUserAPIDatabase(
- b,
- &cfg.UserAPI.AccountDatabase,
- cfg.Global.ServerName,
- cfg.UserAPI.BCryptCost,
- cfg.UserAPI.OpenIDTokenLifetimeMS,
- 0, // TODO
- cfg.Global.ServerNotices.LocalPart,
- )
+func sharedSecretRegister(sharedSecret, serverURL, localpart, password string, admin bool) (accesToken string, err error) {
+ registerURL := fmt.Sprintf("%s/_synapse/admin/v1/register", serverURL)
+ nonceReq, err := http.NewRequest(http.MethodGet, registerURL, nil)
if err != nil {
- logrus.WithError(err).Fatalln("Failed to connect to the database")
+ 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 := io.ReadAll(nonceResp.Body)
+ if err != nil {
+ return "", fmt.Errorf("failed to read response body: %w", err)
+ }
+ defer nonceResp.Body.Close() // nolint: errcheck
+
+ nonce := gjson.GetBytes(body, "nonce").Str
- accType := api.AccountTypeUser
- if *isAdmin {
- accType = api.AccountTypeAdmin
+ adminStr := "notadmin"
+ if admin {
+ adminStr = "admin"
}
+ reg := sharedSecretRegistrationRequest{
+ User: localpart,
+ Password: password,
+ Nonce: nonce,
+ Admin: admin,
+ }
+ macStr, err := getRegisterMac(sharedSecret, nonce, localpart, password, adminStr)
+ if err != nil {
+ return "", err
+ }
+ reg.MacStr = macStr
- available, err := accountDB.CheckAccountAvailability(context.Background(), *username)
+ js, err := json.Marshal(reg)
if err != nil {
- logrus.Fatalln("Unable check username existence.")
+ return "", fmt.Errorf("unable to marshal json: %w", err)
}
- if *resetPassword {
- 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
+ registerReq, err := http.NewRequest(http.MethodPost, registerURL, bytes.NewBuffer(js))
+ if err != nil {
+ return "", fmt.Errorf("unable to create http request: %w", err)
+
}
- if !available {
- logrus.Fatalln("Username is already in use.")
+ regResp, err := cl.Do(registerReq)
+ if err != nil {
+ return "", fmt.Errorf("unable to create account: %w", err)
+ }
+ defer regResp.Body.Close() // nolint: errcheck
+ if regResp.StatusCode < 200 || regResp.StatusCode >= 300 {
+ body, _ = io.ReadAll(regResp.Body)
+ return "", fmt.Errorf(gjson.GetBytes(body, "error").Str)
}
+ r, _ := io.ReadAll(regResp.Body)
- _, err = accountDB.CreateAccount(context.Background(), *username, pass, "", accType)
+ return gjson.GetBytes(r, "access_token").Str, nil
+}
+
+func getRegisterMac(sharedSecret, nonce, localpart, password, adminStr string) (string, error) {
+ joined := strings.Join([]string{nonce, localpart, password, adminStr}, "\x00")
+ mac := hmac.New(sha1.New, []byte(sharedSecret))
+ _, err := mac.Write([]byte(joined))
if err != nil {
- logrus.Fatalln("Failed to create the account:", err.Error())
+ return "", fmt.Errorf("unable to construct mac: %w", err)
}
+ regMac := mac.Sum(nil)
- logrus.Infoln("Created account", *username)
+ return hex.EncodeToString(regMac), nil
}
func getPassword(password, pwdFile string, pwdStdin bool, r io.Reader) (string, error) {
diff --git a/docs/administration/1_createusers.md b/docs/administration/1_createusers.md
index 61ec2299..3468398a 100644
--- a/docs/administration/1_createusers.md
+++ b/docs/administration/1_createusers.md
@@ -14,9 +14,8 @@ User accounts can be created on a Dendrite instance in a number of ways.
The `create-account` tool is built in the `bin` folder when building Dendrite with
the `build.sh` script.
-It uses the `dendrite.yaml` configuration file to connect to the Dendrite user database
-and create the account entries directly. It can therefore be used even if Dendrite is not
-running yet, as long as the database is up.
+It uses the `dendrite.yaml` configuration file to connect to a running Dendrite instance and requires
+shared secret registration to be enabled as explained below.
An example of using `create-account` to create a **normal account**:
@@ -32,6 +31,13 @@ To create a new **admin account**, add the `-admin` flag:
./bin/create-account -config /path/to/dendrite.yaml -username USERNAME -admin
```
+By default `create-account` uses `https://localhost:8448` to connect to Dendrite, this can be overwritten using
+the `-url` flag:
+
+```bash
+./bin/create-account -config /path/to/dendrite.yaml -username USERNAME -url http://localhost:8008
+```
+
An example of using `create-account` when running in **Docker**, having found the `CONTAINERNAME` from `docker ps`:
```bash
diff --git a/docs/administration/4_adminapi.md b/docs/administration/4_adminapi.md
index 51f56374..783fee95 100644
--- a/docs/administration/4_adminapi.md
+++ b/docs/administration/4_adminapi.md
@@ -13,19 +13,32 @@ without warning.
More endpoints will be added in the future.
-## `/_dendrite/admin/evacuateRoom/{roomID}`
+## GET `/_dendrite/admin/evacuateRoom/{roomID}`
This endpoint will instruct Dendrite to part all local users from the given `roomID`
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}`
+## GET `/_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`
+## POST `/_dendrite/admin/resetPassword/{localpart}`
+
+Request body format:
+
+```
+{
+ "password": "new_password_here"
+}
+```
+
+Reset the password of a local user. The `localpart` is the username only, i.e. if
+the full user ID is `@alice:domain.com` then the local part is `alice`.
+
+## GET `/_synapse/admin/v1/register`
Shared secret registration — please see the [user creation page](createusers) for
guidance on configuring and using this endpoint.
diff --git a/internal/httputil/httpapi.go b/internal/httputil/httpapi.go
index aba50ae4..e0436c60 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 388f97cb..66ee9c7c 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 78b226d4..6ba46932 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
}