diff --git a/clientapi/admin_test.go b/clientapi/admin_test.go index 88c0b363..ece14a6d 100644 --- a/clientapi/admin_test.go +++ b/clientapi/admin_test.go @@ -1759,7 +1759,7 @@ func TestAdminUserDeviceDelete(t *testing.T) { } }) - t.Run("Delete non-existing device", func(t *testing.T) { + t.Run("Delete non-existing user's devices", func(t *testing.T) { req := test.NewRequest(t, http.MethodDelete, "/_synapse/admin/v2/users/"+bob.ID+"/devices/anything") req.Header.Set("Authorization", "Bearer "+adminToken) @@ -1774,7 +1774,110 @@ func TestAdminUserDeviceDelete(t *testing.T) { } func TestAdminUserDevicesDelete(t *testing.T) { + alice := test.NewUser(t, test.WithAccountType(uapi.AccountTypeUser)) + bob := test.NewUser(t, test.WithAccountType(uapi.AccountTypeUser)) + adminToken := "superSecretAdminToken" + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + cfg, processCtx, close := testrig.CreateConfig(t, dbType) + defer close() + natsInstance := jetstream.NATSInstance{} + // add a vhost + cfg.Global.VirtualHosts = append(cfg.Global.VirtualHosts, &config.VirtualHost{ + SigningIdentity: fclient.SigningIdentity{ServerName: "vh1"}, + }) + // There's no need to add a full config for msc3861 as we need only an admin token + cfg.ClientAPI.MSCs.MSCs = []string{"msc3861"} + cfg.ClientAPI.MSCs.MSC3861 = &config.MSC3861{AdminToken: adminToken} + + routers := httputil.NewRouters() + cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions) + caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics) + rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics) + rsAPI.SetFederationAPI(nil, nil) + userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) + // We mostly need the userAPI for this test, so nil for other APIs/caches etc. + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, nil, caching.DisableMetrics) + + for _, u := range []*test.User{alice} { + userRes := &uapi.PerformAccountCreationResponse{} + if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{ + AccountType: u.AccountType, + Localpart: u.Localpart, + ServerName: cfg.Global.ServerName, + Password: "", + }, userRes); err != nil { + t.Errorf("failed to create account: %s", err) + } + } + + type payload struct { + Devices []string `json:"devices"` + } + + t.Run("Missing auth token", func(t *testing.T) { + req := test.NewRequest(t, http.MethodPost, "/_synapse/admin/v2/users/"+alice.ID+"/delete_devices") + rec := httptest.NewRecorder() + routers.SynapseAdmin.ServeHTTP(rec, req) + t.Logf("%s", rec.Body.String()) + if rec.Code != http.StatusUnauthorized { + t.Fatalf("expected http status %d, got %d: %s", http.StatusUnauthorized, rec.Code, rec.Body.String()) + } + var b spec.MatrixError + _ = json.NewDecoder(rec.Body).Decode(&b) + if b.ErrCode != spec.ErrorMissingToken { + t.Fatalf("expected error code %s, got %s", spec.ErrorMissingToken, b.ErrCode) + } + }) + + t.Run("Delete existing user's devices", func(t *testing.T) { + var deviceRes uapi.PerformDeviceCreationResponse + if err := userAPI.PerformDeviceCreation(ctx, &uapi.PerformDeviceCreationRequest{ + Localpart: alice.Localpart, + ServerName: cfg.Global.ServerName, + }, &deviceRes); err != nil { + t.Errorf("failed to create account: %s", err) + } + req := test.NewRequest( + t, + http.MethodPost, + "/_synapse/admin/v2/users/"+alice.ID+"/delete_devices", + test.WithJSONBody(t, payload{Devices: []string{deviceRes.Device.ID}}), + ) + req.Header.Set("Authorization", "Bearer "+adminToken) + + rec := httptest.NewRecorder() + routers.SynapseAdmin.ServeHTTP(rec, req) + t.Logf("%s", rec.Body.String()) + if rec.Code != http.StatusOK { + t.Fatalf("expected HTTP status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String()) + } + + var rs uapi.QueryDevicesResponse + _ = userAPI.QueryDevices(ctx, &uapi.QueryDevicesRequest{UserID: alice.ID}, &rs) + if len(rs.Devices) > 0 { + t.Errorf("expected 0 devices, got %d", len(rs.Devices)) + } + }) + + t.Run("Delete non-existing user's devices", func(t *testing.T) { + req := test.NewRequest( + t, + http.MethodPost, + "/_synapse/admin/v2/users/"+bob.ID+"/delete_devices", + test.WithJSONBody(t, payload{Devices: []string{"anyDevID"}}), + ) + req.Header.Set("Authorization", "Bearer "+adminToken) + + rec := httptest.NewRecorder() + routers.SynapseAdmin.ServeHTTP(rec, req) + t.Logf("%s", rec.Body.String()) + if rec.Code != http.StatusOK { + t.Fatalf("expected HTTP status %d, got %d: %s", http.StatusOK, rec.Code, rec.Body.String()) + } + }) + }) } func TestAdminDeactivateAccount(t *testing.T) { diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go index 3946964d..02940128 100644 --- a/clientapi/routing/admin.go +++ b/clientapi/routing/admin.go @@ -720,11 +720,13 @@ func AdminUserDevicesDelete( } userID := vars["userID"] + if req.Body == nil { + return util.MessageResponse(http.StatusBadRequest, "body is required") + } var payload struct { Devices []string `json:"devices"` } - defer req.Body.Close() // nolint: errcheck if err = json.NewDecoder(req.Body).Decode(&payload); err != nil { logger.WithError(err).Error("unable to decode device deletion request") return util.JSONResponse{ @@ -732,6 +734,7 @@ func AdminUserDevicesDelete( JSON: spec.InternalServerError{}, } } + defer req.Body.Close() // nolint: errcheck { // XXX: this response struct can completely removed everywhere as it doesn't