mirror of
https://github.com/hoernschen/dendrite.git
synced 2024-12-27 07:28:27 +00:00
Add tests for CSAPI membership changes (#3034)
Adds some more checks in regards to power levels, uses a less heavy way to get the membership of a user, avoids asking the database for the room version, since it will be queried later. [skip ci]
This commit is contained in:
parent
c2db38d295
commit
560ba46272
3 changed files with 412 additions and 131 deletions
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
"github.com/matrix-org/dendrite/setup/base"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/test"
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
@ -600,3 +601,295 @@ func TestSetAvatarURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTyping(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
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{}
|
||||||
|
|
||||||
|
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)
|
||||||
|
// Needed to create accounts
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
// We mostly need the rsAPI/userAPI for this test, so nil for other APIs 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]userDevice{
|
||||||
|
alice: {},
|
||||||
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
if err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
typingForUser string
|
||||||
|
roomID string
|
||||||
|
requestBody io.Reader
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "can not set typing for different user",
|
||||||
|
typingForUser: "@notourself:test",
|
||||||
|
roomID: room.ID,
|
||||||
|
requestBody: strings.NewReader(""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid request body",
|
||||||
|
typingForUser: alice.ID,
|
||||||
|
roomID: room.ID,
|
||||||
|
requestBody: strings.NewReader(""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-existent room",
|
||||||
|
typingForUser: alice.ID,
|
||||||
|
roomID: "!doesnotexist:test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid room ID",
|
||||||
|
typingForUser: alice.ID,
|
||||||
|
roomID: "@notaroomid:test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allowed to set own typing status",
|
||||||
|
typingForUser: alice.ID,
|
||||||
|
roomID: room.ID,
|
||||||
|
requestBody: strings.NewReader(`{"typing":true}`),
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/rooms/"+tc.roomID+"/typing/"+tc.typingForUser, tc.requestBody)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if tc.wantOK && rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMembership(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
ctx := context.Background()
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
|
cfg.ClientAPI.RateLimiting.Enabled = false
|
||||||
|
defer close()
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
|
||||||
|
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)
|
||||||
|
// Needed to create accounts
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil)
|
||||||
|
rsAPI.SetUserAPI(userAPI)
|
||||||
|
// We mostly need the rsAPI/userAPI for this test, so nil for other APIs 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]userDevice{
|
||||||
|
alice: {},
|
||||||
|
bob: {},
|
||||||
|
}
|
||||||
|
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
if err := api.SendEvents(ctx, rsAPI, api.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidBodyRequest := func(roomID, membershipType string) *http.Request {
|
||||||
|
return httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", roomID, membershipType), strings.NewReader(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
missingUserIDRequest := func(roomID, membershipType string) *http.Request {
|
||||||
|
return httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", roomID, membershipType), strings.NewReader("{}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
roomID string
|
||||||
|
request *http.Request
|
||||||
|
wantOK bool
|
||||||
|
asUser *test.User
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ban - invalid request body",
|
||||||
|
request: invalidBodyRequest(room.ID, "ban"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kick - invalid request body",
|
||||||
|
request: invalidBodyRequest(room.ID, "kick"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unban - invalid request body",
|
||||||
|
request: invalidBodyRequest(room.ID, "unban"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invite - invalid request body",
|
||||||
|
request: invalidBodyRequest(room.ID, "invite"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ban - missing user_id body",
|
||||||
|
request: missingUserIDRequest(room.ID, "ban"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kick - missing user_id body",
|
||||||
|
request: missingUserIDRequest(room.ID, "kick"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unban - missing user_id body",
|
||||||
|
request: missingUserIDRequest(room.ID, "unban"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invite - missing user_id body",
|
||||||
|
request: missingUserIDRequest(room.ID, "invite"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob forgets invalid room",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", "!doesnotexist", "forget"), strings.NewReader("")),
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can not ban Bob in non-existent room", // fails because "not joined"
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", "!doesnotexist:test", "ban"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can not kick Bob in non-existent room", // fails because "not joined"
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", "!doesnotexist:test", "kick"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
},
|
||||||
|
// the following must run in sequence, as they build up on each other
|
||||||
|
{
|
||||||
|
name: "Alice invites Bob",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "invite"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob accepts invite",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "join"), strings.NewReader("")),
|
||||||
|
wantOK: true,
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice verifies that Bob is joined", // returns an error if no membership event can be found
|
||||||
|
request: httptest.NewRequest(http.MethodGet, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s/m.room.member/%s", room.ID, "state", bob.ID), strings.NewReader("")),
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob forgets the room but is still a member",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "forget"), strings.NewReader("")),
|
||||||
|
wantOK: false, // user is still in the room
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob can not kick Alice",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "kick"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, alice.ID))),
|
||||||
|
wantOK: false, // powerlevel too low
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob can not ban Alice",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "ban"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, alice.ID))),
|
||||||
|
wantOK: false, // powerlevel too low
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can kick Bob",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "kick"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can ban Bob",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "ban"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can not kick Bob again",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "kick"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: false, // can not kick banned/left user
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob can not unban himself", // mostly because of not being a member of the room
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "unban"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can not invite Bob again",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "invite"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: false, // user still banned
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can unban Bob",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "unban"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can not unban Bob again",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "unban"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alice can invite Bob again",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "invite"), strings.NewReader(fmt.Sprintf(`{"user_id":"%s"}`, bob.ID))),
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob can reject the invite by leaving",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "leave"), strings.NewReader("")),
|
||||||
|
wantOK: true,
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob can forget the room",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "forget"), strings.NewReader("")),
|
||||||
|
wantOK: true,
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob can forget the room again",
|
||||||
|
request: httptest.NewRequest(http.MethodPost, fmt.Sprintf("/_matrix/client/v3/rooms/%s/%s", room.ID, "forget"), strings.NewReader("")),
|
||||||
|
wantOK: true,
|
||||||
|
asUser: bob,
|
||||||
|
},
|
||||||
|
// END must run in sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.asUser == nil {
|
||||||
|
tc.asUser = alice
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
tc.request.Header.Set("Authorization", "Bearer "+accessTokens[tc.asUser].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, tc.request)
|
||||||
|
if tc.wantOK && rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if !tc.wantOK && rec.Code == http.StatusOK {
|
||||||
|
t.Fatalf("expected request to fail, but didn't: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
t.Logf("%s", rec.Body.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -16,11 +16,12 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
@ -31,76 +32,56 @@ import (
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errMissingUserID = errors.New("'user_id' must be supplied")
|
|
||||||
|
|
||||||
func SendBan(
|
func SendBan(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if body.UserID == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("missing user_id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
||||||
EventType: gomatrixserverlib.MRoomPowerLevels,
|
if errRes != nil {
|
||||||
StateKey: "",
|
return *errRes
|
||||||
})
|
|
||||||
if plEvent == nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 403,
|
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to ban this user, no power_levels event in this room."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pl, err := plEvent.PowerLevels()
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 403,
|
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to ban this user, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
allowedToBan := pl.UserLevel(device.UserID) >= pl.Ban
|
allowedToBan := pl.UserLevel(device.UserID) >= pl.Ban
|
||||||
if !allowedToBan {
|
if !allowedToBan {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("You don't have permission to ban this user, power level too low."),
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, power level too low."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, "ban", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, gomatrixserverlib.Ban, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
||||||
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
||||||
roomVer gomatrixserverlib.RoomVersion,
|
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse {
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse {
|
||||||
|
|
||||||
event, err := buildMembershipEvent(
|
event, err := buildMembershipEvent(
|
||||||
ctx, targetUserID, reason, profileAPI, device, membership,
|
ctx, targetUserID, reason, profileAPI, device, membership,
|
||||||
roomID, false, cfg, evTime, rsAPI, asAPI,
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
||||||
)
|
)
|
||||||
if err == errMissingUserID {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
|
||||||
}
|
|
||||||
} else if err == eventutil.ErrRoomNoExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound(err.Error()),
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
@ -109,7 +90,7 @@ func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, devic
|
||||||
if err = roomserverAPI.SendEvents(
|
if err = roomserverAPI.SendEvents(
|
||||||
ctx, rsAPI,
|
ctx, rsAPI,
|
||||||
roomserverAPI.KindNew,
|
roomserverAPI.KindNew,
|
||||||
[]*gomatrixserverlib.HeaderedEvent{event.Event.Headered(roomVer)},
|
[]*gomatrixserverlib.HeaderedEvent{event},
|
||||||
device.UserDomain(),
|
device.UserDomain(),
|
||||||
serverName,
|
serverName,
|
||||||
serverName,
|
serverName,
|
||||||
|
@ -131,13 +112,65 @@ func SendKick(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
if body.UserID == "" {
|
if body.UserID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("missing user_id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
|
||||||
|
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
allowedToKick := pl.UserLevel(device.UserID) >= pl.Kick
|
||||||
|
if !allowedToKick {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to kick this user, power level too low."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
|
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
UserID: body.UserID,
|
||||||
|
}, &queryRes)
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
||||||
|
if queryRes.Membership != gomatrixserverlib.Join && queryRes.Membership != gomatrixserverlib.Invite {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Unknown("cannot /kick banned or left users"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: should we be using SendLeave instead?
|
||||||
|
return sendMembership(req.Context(), profileAPI, device, roomID, gomatrixserverlib.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendUnban(
|
||||||
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
||||||
|
roomID string, cfg *config.ClientAPI,
|
||||||
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
|
if reqErr != nil {
|
||||||
|
return *reqErr
|
||||||
|
}
|
||||||
|
if body.UserID == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("missing user_id"),
|
JSON: jsonerror.BadJSON("missing user_id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,56 +188,16 @@ func SendKick(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
|
||||||
if queryRes.Membership != "join" && queryRes.Membership != "invite" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 403,
|
|
||||||
JSON: jsonerror.Unknown("cannot /kick banned or left users"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: should we be using SendLeave instead?
|
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendUnban(
|
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
|
||||||
roomID string, cfg *config.ClientAPI,
|
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
|
||||||
) util.JSONResponse {
|
|
||||||
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
|
||||||
if reqErr != nil {
|
|
||||||
return *reqErr
|
|
||||||
}
|
|
||||||
if body.UserID == "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 400,
|
|
||||||
JSON: jsonerror.BadJSON("missing user_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
|
||||||
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
UserID: body.UserID,
|
|
||||||
}, &queryRes)
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
if !queryRes.RoomExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonerror.Forbidden("room does not exist"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// unban is only valid if the user is currently banned
|
// unban is only valid if the user is currently banned
|
||||||
if queryRes.Membership != "ban" {
|
if queryRes.Membership != gomatrixserverlib.Ban {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("can only /unban users that are banned"),
|
JSON: jsonerror.Unknown("can only /unban users that are banned"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: should we be using SendLeave instead?
|
// TODO: should we be using SendLeave instead?
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, gomatrixserverlib.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendInvite(
|
func SendInvite(
|
||||||
|
@ -212,7 +205,7 @@ func SendInvite(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, _, reqErr := extractRequestData(req, roomID, rsAPI)
|
body, evTime, reqErr := extractRequestData(req)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
@ -234,6 +227,18 @@ func SendInvite(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if body.UserID == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("missing user_id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
|
||||||
// We already received the return value, so no need to check for an error here.
|
// We already received the return value, so no need to check for an error here.
|
||||||
response, _ := sendInvite(req.Context(), profileAPI, device, roomID, body.UserID, body.Reason, cfg, rsAPI, asAPI, evTime)
|
response, _ := sendInvite(req.Context(), profileAPI, device, roomID, body.UserID, body.Reason, cfg, rsAPI, asAPI, evTime)
|
||||||
return response
|
return response
|
||||||
|
@ -250,20 +255,10 @@ func sendInvite(
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time,
|
asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time,
|
||||||
) (util.JSONResponse, error) {
|
) (util.JSONResponse, error) {
|
||||||
event, err := buildMembershipEvent(
|
event, err := buildMembershipEvent(
|
||||||
ctx, userID, reason, profileAPI, device, "invite",
|
ctx, userID, reason, profileAPI, device, gomatrixserverlib.Invite,
|
||||||
roomID, false, cfg, evTime, rsAPI, asAPI,
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
||||||
)
|
)
|
||||||
if err == errMissingUserID {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
|
||||||
}, err
|
|
||||||
} else if err == eventutil.ErrRoomNoExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound(err.Error()),
|
|
||||||
}, err
|
|
||||||
} else if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
return jsonerror.InternalServerError(), err
|
return jsonerror.InternalServerError(), err
|
||||||
}
|
}
|
||||||
|
@ -357,19 +352,7 @@ func loadProfile(
|
||||||
return profile, err
|
return profile, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractRequestData(req *http.Request, roomID string, rsAPI roomserverAPI.ClientRoomserverAPI) (
|
func extractRequestData(req *http.Request) (body *threepid.MembershipRequest, evTime time.Time, resErr *util.JSONResponse) {
|
||||||
body *threepid.MembershipRequest, evTime time.Time, roomVer gomatrixserverlib.RoomVersion, resErr *util.JSONResponse,
|
|
||||||
) {
|
|
||||||
verReq := roomserverAPI.QueryRoomVersionForRoomRequest{RoomID: roomID}
|
|
||||||
verRes := roomserverAPI.QueryRoomVersionForRoomResponse{}
|
|
||||||
if err := rsAPI.QueryRoomVersionForRoom(req.Context(), &verReq, &verRes); err != nil {
|
|
||||||
resErr = &util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.UnsupportedRoomVersion(err.Error()),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roomVer = verRes.RoomVersion
|
|
||||||
|
|
||||||
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
||||||
resErr = reqErr
|
resErr = reqErr
|
||||||
|
@ -432,34 +415,17 @@ func checkAndProcessThreepid(
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID, roomID string) *util.JSONResponse {
|
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID, roomID string) *util.JSONResponse {
|
||||||
tuple := gomatrixserverlib.StateKeyTuple{
|
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
EventType: gomatrixserverlib.MRoomMember,
|
err := rsAPI.QueryMembershipForUser(ctx, &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
StateKey: userID,
|
RoomID: roomID,
|
||||||
}
|
UserID: userID,
|
||||||
var membershipRes roomserverAPI.QueryCurrentStateResponse
|
|
||||||
err := rsAPI.QueryCurrentState(ctx, &roomserverAPI.QueryCurrentStateRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
StateTuples: []gomatrixserverlib.StateKeyTuple{tuple},
|
|
||||||
}, &membershipRes)
|
}, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("QueryCurrentState: could not query membership for user")
|
util.GetLogger(ctx).WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
||||||
e := jsonerror.InternalServerError()
|
e := jsonerror.InternalServerError()
|
||||||
return &e
|
return &e
|
||||||
}
|
}
|
||||||
ev := membershipRes.StateEvents[tuple]
|
if !membershipRes.IsInRoom {
|
||||||
if ev == nil {
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonerror.Forbidden("user does not belong to room"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
membership, err := ev.Membership()
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("Member event isn't valid")
|
|
||||||
e := jsonerror.InternalServerError()
|
|
||||||
return &e
|
|
||||||
}
|
|
||||||
if membership != gomatrixserverlib.Join {
|
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("user does not belong to room"),
|
JSON: jsonerror.Forbidden("user does not belong to room"),
|
||||||
|
@ -511,3 +477,24 @@ func SendForget(
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPowerlevels(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, roomID string) (*gomatrixserverlib.PowerLevelContent, *util.JSONResponse) {
|
||||||
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
||||||
|
EventType: gomatrixserverlib.MRoomPowerLevels,
|
||||||
|
StateKey: "",
|
||||||
|
})
|
||||||
|
if plEvent == nil {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to perform this action, no power_levels event in this room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pl, err := plEvent.PowerLevels()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to perform this action, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pl, nil
|
||||||
|
}
|
||||||
|
|
|
@ -15,12 +15,13 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type typingContentJSON struct {
|
type typingContentJSON struct {
|
||||||
|
|
Loading…
Reference in a new issue