aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTill <2353100+S7evinK@users.noreply.github.com>2023-03-27 15:39:33 +0200
committerGitHub <noreply@github.com>2023-03-27 15:39:33 +0200
commitfa7710315a00f6e857bb9c315c0a7ba248288b79 (patch)
treebab330b1538667f4d310570897d179cb7f333eef
parente8b2162a01bf0e735869d5a2b9be258cb380255e (diff)
Add tests for the Dendrite admin APIs (#3028)
Contains a breaking change, since the endpoints `/_dendrite/admin/evacuateRoom/{roomID}` and `/_dendrite/admin/evacuateUser/{userID}` are now using `POST` instead of `GET`
-rw-r--r--clientapi/admin_test.go326
-rw-r--r--clientapi/routing/admin.go31
-rw-r--r--clientapi/routing/routing.go8
-rw-r--r--docs/administration/4_adminapi.md4
-rw-r--r--roomserver/internal/perform/perform_admin.go3
5 files changed, 284 insertions, 88 deletions
diff --git a/clientapi/admin_test.go b/clientapi/admin_test.go
index 4d2bf67b..3e7cb875 100644
--- a/clientapi/admin_test.go
+++ b/clientapi/admin_test.go
@@ -4,6 +4,7 @@ import (
"context"
"net/http"
"net/http/httptest"
+ "reflect"
"testing"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/roomserver"
"github.com/matrix-org/dendrite/roomserver/api"
+ basepkg "github.com/matrix-org/dendrite/setup/base"
"github.com/matrix-org/dendrite/setup/config"
"github.com/matrix-org/dendrite/setup/jetstream"
"github.com/matrix-org/dendrite/syncapi"
@@ -57,34 +59,7 @@ func TestAdminResetPassword(t *testing.T) {
bob: "",
vhUser: "",
}
- for u := range accessTokens {
- localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
- userRes := &uapi.PerformAccountCreationResponse{}
- password := util.RandomString(8)
- if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
- AccountType: u.AccountType,
- Localpart: localpart,
- ServerName: serverName,
- Password: password,
- }, userRes); err != nil {
- t.Errorf("failed to create account: %s", err)
- }
-
- req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
- "type": authtypes.LoginTypePassword,
- "identifier": map[string]interface{}{
- "type": "m.id.user",
- "user": u.ID,
- },
- "password": password,
- }))
- rec := httptest.NewRecorder()
- routers.Client.ServeHTTP(rec, req)
- if rec.Code != http.StatusOK {
- t.Fatalf("failed to login: %s", rec.Body.String())
- }
- accessTokens[u] = gjson.GetBytes(rec.Body.Bytes(), "access_token").String()
- }
+ createAccessTokens(t, accessTokens, userAPI, ctx, routers)
testCases := []struct {
name string
@@ -182,34 +157,7 @@ func TestPurgeRoom(t *testing.T) {
accessTokens := map[*test.User]string{
aliceAdmin: "",
}
- for u := range accessTokens {
- localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
- userRes := &uapi.PerformAccountCreationResponse{}
- password := util.RandomString(8)
- if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
- AccountType: u.AccountType,
- Localpart: localpart,
- ServerName: serverName,
- Password: password,
- }, userRes); err != nil {
- t.Errorf("failed to create account: %s", err)
- }
-
- req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
- "type": authtypes.LoginTypePassword,
- "identifier": map[string]interface{}{
- "type": "m.id.user",
- "user": u.ID,
- },
- "password": password,
- }))
- rec := httptest.NewRecorder()
- routers.Client.ServeHTTP(rec, req)
- if rec.Code != http.StatusOK {
- t.Fatalf("failed to login: %s", rec.Body.String())
- }
- accessTokens[u] = gjson.GetBytes(rec.Body.Bytes(), "access_token").String()
- }
+ createAccessTokens(t, accessTokens, userAPI, ctx, routers)
testCases := []struct {
name string
@@ -239,3 +187,269 @@ func TestPurgeRoom(t *testing.T) {
})
}
+
+func TestAdminEvacuateRoom(t *testing.T) {
+ aliceAdmin := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
+ bob := test.NewUser(t)
+ room := test.NewRoom(t, aliceAdmin)
+
+ // Join Bob
+ room.CreateAndInsert(t, bob, gomatrixserverlib.MRoomMember, map[string]interface{}{
+ "membership": "join",
+ }, test.WithStateKey(bob.ID))
+
+ ctx := context.Background()
+
+ test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
+ cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+ caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+ natsInstance := jetstream.NATSInstance{}
+ defer close()
+
+ routers := httputil.NewRouters()
+ cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+ rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+ userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
+
+ // this starts the JetStream consumers
+ fsAPI := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, nil, rsAPI, caches, nil, true)
+ rsAPI.SetFederationAPI(fsAPI, nil)
+
+ // Create the room
+ if err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", api.DoNotSendToOtherServers, nil, false); err != nil {
+ t.Fatalf("failed to send events: %v", err)
+ }
+
+ // We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
+ AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
+
+ // Create the users in the userapi and login
+ accessTokens := map[*test.User]string{
+ aliceAdmin: "",
+ }
+ createAccessTokens(t, accessTokens, userAPI, ctx, routers)
+
+ testCases := []struct {
+ name string
+ roomID string
+ wantOK bool
+ wantAffected []string
+ }{
+ {name: "Can evacuate existing room", wantOK: true, roomID: room.ID, wantAffected: []string{aliceAdmin.ID, bob.ID}},
+ {name: "Can not evacuate non-existent room", wantOK: false, roomID: "!doesnotexist:localhost", wantAffected: []string{}},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/evacuateRoom/"+tc.roomID)
+
+ req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin])
+
+ rec := httptest.NewRecorder()
+ routers.DendriteAdmin.ServeHTTP(rec, req)
+ t.Logf("%s", rec.Body.String())
+ if tc.wantOK && rec.Code != http.StatusOK {
+ t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
+ }
+
+ affectedArr := gjson.GetBytes(rec.Body.Bytes(), "affected").Array()
+ affected := make([]string, 0, len(affectedArr))
+ for _, x := range affectedArr {
+ affected = append(affected, x.Str)
+ }
+ if !reflect.DeepEqual(affected, tc.wantAffected) {
+ t.Fatalf("expected affected %#v, but got %#v", tc.wantAffected, affected)
+ }
+ })
+ }
+ })
+}
+
+func TestAdminEvacuateUser(t *testing.T) {
+ aliceAdmin := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
+ bob := test.NewUser(t)
+ room := test.NewRoom(t, aliceAdmin)
+ room2 := test.NewRoom(t, aliceAdmin)
+
+ // Join Bob
+ room.CreateAndInsert(t, bob, gomatrixserverlib.MRoomMember, map[string]interface{}{
+ "membership": "join",
+ }, test.WithStateKey(bob.ID))
+ room2.CreateAndInsert(t, bob, gomatrixserverlib.MRoomMember, map[string]interface{}{
+ "membership": "join",
+ }, test.WithStateKey(bob.ID))
+
+ ctx := context.Background()
+
+ test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
+ cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+ caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+ natsInstance := jetstream.NATSInstance{}
+ defer close()
+
+ routers := httputil.NewRouters()
+ cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+ rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+ userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
+
+ // this starts the JetStream consumers
+ fsAPI := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, basepkg.CreateFederationClient(cfg, nil), rsAPI, caches, nil, true)
+ rsAPI.SetFederationAPI(fsAPI, nil)
+
+ // Create the room
+ if err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", api.DoNotSendToOtherServers, nil, false); err != nil {
+ t.Fatalf("failed to send events: %v", err)
+ }
+ if err := api.SendEvents(ctx, rsAPI, api.KindNew, room2.Events(), "test", "test", api.DoNotSendToOtherServers, nil, false); err != nil {
+ t.Fatalf("failed to send events: %v", err)
+ }
+
+ // We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
+ AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
+
+ // Create the users in the userapi and login
+ accessTokens := map[*test.User]string{
+ aliceAdmin: "",
+ }
+ createAccessTokens(t, accessTokens, userAPI, ctx, routers)
+
+ testCases := []struct {
+ name string
+ userID string
+ wantOK bool
+ wantAffectedRooms []string
+ }{
+ {name: "Can evacuate existing user", wantOK: true, userID: bob.ID, wantAffectedRooms: []string{room.ID, room2.ID}},
+ {name: "invalid userID is rejected", wantOK: false, userID: "!notauserid:test", wantAffectedRooms: []string{}},
+ {name: "Can not evacuate user from different server", wantOK: false, userID: "@doesnotexist:localhost", wantAffectedRooms: []string{}},
+ {name: "Can not evacuate non-existent user", wantOK: false, userID: "@doesnotexist:test", wantAffectedRooms: []string{}},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/evacuateUser/"+tc.userID)
+
+ req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin])
+
+ rec := httptest.NewRecorder()
+ routers.DendriteAdmin.ServeHTTP(rec, req)
+ t.Logf("%s", rec.Body.String())
+ if tc.wantOK && rec.Code != http.StatusOK {
+ t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
+ }
+
+ affectedArr := gjson.GetBytes(rec.Body.Bytes(), "affected").Array()
+ affected := make([]string, 0, len(affectedArr))
+ for _, x := range affectedArr {
+ affected = append(affected, x.Str)
+ }
+ if !reflect.DeepEqual(affected, tc.wantAffectedRooms) {
+ t.Fatalf("expected affected %#v, but got %#v", tc.wantAffectedRooms, affected)
+ }
+
+ })
+ }
+ // Wait for the FS API to have consumed every message
+ js, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
+ timeout := time.After(time.Second)
+ for {
+ select {
+ case <-timeout:
+ t.Fatalf("FS API didn't process all events in time")
+ default:
+ }
+ info, err := js.ConsumerInfo(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent), cfg.Global.JetStream.Durable("FederationAPIRoomServerConsumer")+"Pull")
+ if err != nil {
+ time.Sleep(time.Millisecond * 10)
+ continue
+ }
+ if info.NumPending == 0 && info.NumAckPending == 0 {
+ break
+ }
+ }
+ })
+}
+
+func TestAdminMarkAsStale(t *testing.T) {
+ aliceAdmin := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
+
+ ctx := context.Background()
+
+ test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
+ cfg, processCtx, close := testrig.CreateConfig(t, dbType)
+ caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
+ natsInstance := jetstream.NATSInstance{}
+ defer close()
+
+ routers := httputil.NewRouters()
+ cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
+ rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
+ userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
+
+ // We mostly need the rsAPI for this test, so nil for other APIs/caches etc.
+ AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
+
+ // Create the users in the userapi and login
+ accessTokens := map[*test.User]string{
+ aliceAdmin: "",
+ }
+ createAccessTokens(t, accessTokens, userAPI, ctx, routers)
+
+ testCases := []struct {
+ name string
+ userID string
+ wantOK bool
+ }{
+ {name: "local user is not allowed", userID: aliceAdmin.ID},
+ {name: "invalid userID", userID: "!notvalid:test"},
+ {name: "remote user is allowed", userID: "@alice:localhost", wantOK: true},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/refreshDevices/"+tc.userID)
+
+ req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin])
+
+ rec := httptest.NewRecorder()
+ routers.DendriteAdmin.ServeHTTP(rec, req)
+ t.Logf("%s", rec.Body.String())
+ if tc.wantOK && rec.Code != http.StatusOK {
+ t.Fatalf("expected http status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String())
+ }
+ })
+ }
+ })
+}
+
+func createAccessTokens(t *testing.T, accessTokens map[*test.User]string, userAPI uapi.UserInternalAPI, ctx context.Context, routers httputil.Routers) {
+ t.Helper()
+ for u := range accessTokens {
+ localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
+ userRes := &uapi.PerformAccountCreationResponse{}
+ password := util.RandomString(8)
+ if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
+ AccountType: u.AccountType,
+ Localpart: localpart,
+ ServerName: serverName,
+ Password: password,
+ }, userRes); err != nil {
+ t.Errorf("failed to create account: %s", err)
+ }
+
+ req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
+ "type": authtypes.LoginTypePassword,
+ "identifier": map[string]interface{}{
+ "type": "m.id.user",
+ "user": u.ID,
+ },
+ "password": password,
+ }))
+ rec := httptest.NewRecorder()
+ routers.Client.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("failed to login: %s", rec.Body.String())
+ }
+ accessTokens[u] = gjson.GetBytes(rec.Body.Bytes(), "access_token").String()
+ }
+}
diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go
index a01f6b94..76e18f2f 100644
--- a/clientapi/routing/admin.go
+++ b/clientapi/routing/admin.go
@@ -22,23 +22,16 @@ import (
"github.com/matrix-org/dendrite/userapi/api"
)
-func AdminEvacuateRoom(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
+func AdminEvacuateRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
if err != nil {
return util.ErrorResponse(err)
}
- roomID, ok := vars["roomID"]
- if !ok {
- return util.JSONResponse{
- Code: http.StatusBadRequest,
- JSON: jsonerror.MissingArgument("Expecting room ID."),
- }
- }
res := &roomserverAPI.PerformAdminEvacuateRoomResponse{}
if err := rsAPI.PerformAdminEvacuateRoom(
req.Context(),
&roomserverAPI.PerformAdminEvacuateRoomRequest{
- RoomID: roomID,
+ RoomID: vars["roomID"],
},
res,
); err != nil {
@@ -55,18 +48,13 @@ func AdminEvacuateRoom(req *http.Request, cfg *config.ClientAPI, device *api.Dev
}
}
-func AdminEvacuateUser(req *http.Request, cfg *config.ClientAPI, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
+func AdminEvacuateUser(req *http.Request, cfg *config.ClientAPI, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
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."),
- }
- }
+ userID := vars["userID"]
+
_, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return util.MessageResponse(http.StatusBadRequest, err.Error())
@@ -103,13 +91,8 @@ func AdminPurgeRoom(req *http.Request, cfg *config.ClientAPI, device *api.Device
if err != nil {
return util.ErrorResponse(err)
}
- roomID, ok := vars["roomID"]
- if !ok {
- return util.JSONResponse{
- Code: http.StatusBadRequest,
- JSON: jsonerror.MissingArgument("Expecting room ID."),
- }
- }
+ roomID := vars["roomID"]
+
res := &roomserverAPI.PerformAdminPurgeRoomResponse{}
if err := rsAPI.PerformAdminPurgeRoom(
context.Background(),
diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go
index 6a86980d..e261cb3a 100644
--- a/clientapi/routing/routing.go
+++ b/clientapi/routing/routing.go
@@ -155,15 +155,15 @@ func Setup(
dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}",
httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
- return AdminEvacuateRoom(req, cfg, device, rsAPI)
+ return AdminEvacuateRoom(req, rsAPI)
}),
- ).Methods(http.MethodGet, http.MethodOptions)
+ ).Methods(http.MethodPost, http.MethodOptions)
dendriteAdminRouter.Handle("/admin/evacuateUser/{userID}",
httputil.MakeAdminAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
- return AdminEvacuateUser(req, cfg, device, rsAPI)
+ return AdminEvacuateUser(req, cfg, rsAPI)
}),
- ).Methods(http.MethodGet, http.MethodOptions)
+ ).Methods(http.MethodPost, http.MethodOptions)
dendriteAdminRouter.Handle("/admin/purgeRoom/{roomID}",
httputil.MakeAdminAPI("admin_purge_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
diff --git a/docs/administration/4_adminapi.md b/docs/administration/4_adminapi.md
index 46cfac22..b11aeb1a 100644
--- a/docs/administration/4_adminapi.md
+++ b/docs/administration/4_adminapi.md
@@ -32,7 +32,7 @@ UPDATE userapi_accounts SET account_type = 3 WHERE localpart = '$localpart';
Where `$localpart` is the username only (e.g. `alice`).
-## GET `/_dendrite/admin/evacuateRoom/{roomID}`
+## POST `/_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
@@ -41,7 +41,7 @@ the user IDs of all affected users.
If the room has an alias set (e.g. is published), the room's ID will not be visible in the URL, but it can
be found as the room's "internal ID" in Element Web (Settings -> Advanced)
-## GET `/_dendrite/admin/evacuateUser/{userID}`
+## POST `/_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
diff --git a/roomserver/internal/perform/perform_admin.go b/roomserver/internal/perform/perform_admin.go
index 45089bdd..0f124911 100644
--- a/roomserver/internal/perform/perform_admin.go
+++ b/roomserver/internal/perform/perform_admin.go
@@ -227,6 +227,7 @@ func (r *Admin) PerformAdminEvacuateUser(
}
return nil
}
+ res.Affected = append(res.Affected, roomID)
if len(outputEvents) == 0 {
continue
}
@@ -237,8 +238,6 @@ func (r *Admin) PerformAdminEvacuateUser(
}
return nil
}
-
- res.Affected = append(res.Affected, roomID)
}
return nil
}