Merge branch 'main' of github.com:matrix-org/dendrite into s7evink/membershiptests

This commit is contained in:
Till Faelligen 2023-04-03 20:31:52 +02:00
commit 5e8a1c6941
No known key found for this signature in database
GPG key ID: 3DF82D8AB9211D4E
30 changed files with 1339 additions and 1474 deletions

View file

@ -6,6 +6,7 @@ on:
- main
paths:
- 'helm/**' # only execute if we have helm chart changes
workflow_dispatch:
jobs:
release:

View file

@ -22,8 +22,6 @@ import (
"encoding/json"
"errors"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
userapi "github.com/matrix-org/dendrite/userapi/api"
)
@ -150,6 +148,10 @@ type ASLocationResponse struct {
Fields json.RawMessage `json:"fields"`
}
// ErrProfileNotExists is returned when trying to lookup a user's profile that
// doesn't exist locally.
var ErrProfileNotExists = errors.New("no known profile for given user ID")
// RetrieveUserProfile is a wrapper that queries both the local database and
// application services for a given user's profile
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
@ -157,25 +159,11 @@ func RetrieveUserProfile(
ctx context.Context,
userID string,
asAPI AppServiceInternalAPI,
profileAPI userapi.ClientUserAPI,
profileAPI userapi.ProfileAPI,
) (*authtypes.Profile, error) {
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return nil, err
}
// Try to query the user from the local database
res := &userapi.QueryProfileResponse{}
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
if err != nil {
return nil, err
}
profile := &authtypes.Profile{
Localpart: localpart,
DisplayName: res.DisplayName,
AvatarURL: res.AvatarURL,
}
if res.UserExists {
profile, err := profileAPI.QueryProfile(ctx, userID)
if err == nil {
return profile, nil
}
@ -188,19 +176,15 @@ func RetrieveUserProfile(
// If no user exists, return
if !userResp.UserIDExists {
return nil, errors.New("no known profile for given user ID")
return nil, ErrProfileNotExists
}
// Try to query the user from the local database again
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
profile, err = profileAPI.QueryProfile(ctx, userID)
if err != nil {
return nil, err
}
// profile should not be nil at this point
return &authtypes.Profile{
Localpart: localpart,
DisplayName: res.DisplayName,
AvatarURL: res.AvatarURL,
}, nil
return profile, nil
}

View file

@ -8,7 +8,6 @@ import (
"testing"
"time"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/federationapi"
"github.com/matrix-org/dendrite/internal/caching"
"github.com/matrix-org/dendrite/internal/httputil"
@ -54,10 +53,10 @@ func TestAdminResetPassword(t *testing.T) {
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: "",
bob: "",
vhUser: "",
accessTokens := map[*test.User]userDevice{
aliceAdmin: {},
bob: {},
vhUser: {},
}
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
@ -103,7 +102,7 @@ func TestAdminResetPassword(t *testing.T) {
}
if tc.withHeader {
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.requestingUser])
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.requestingUser].accessToken)
}
rec := httptest.NewRecorder()
@ -154,8 +153,8 @@ func TestPurgeRoom(t *testing.T) {
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: "",
accessTokens := map[*test.User]userDevice{
aliceAdmin: {},
}
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
@ -174,7 +173,7 @@ func TestPurgeRoom(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
req := test.NewRequest(t, http.MethodPost, "/_dendrite/admin/purgeRoom/"+tc.roomID)
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin])
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
rec := httptest.NewRecorder()
routers.DendriteAdmin.ServeHTTP(rec, req)
@ -224,8 +223,8 @@ func TestAdminEvacuateRoom(t *testing.T) {
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: "",
accessTokens := map[*test.User]userDevice{
aliceAdmin: {},
}
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
@ -243,7 +242,7 @@ func TestAdminEvacuateRoom(t *testing.T) {
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])
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
rec := httptest.NewRecorder()
routers.DendriteAdmin.ServeHTTP(rec, req)
@ -262,6 +261,25 @@ func TestAdminEvacuateRoom(t *testing.T) {
}
})
}
// 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
}
}
})
}
@ -308,8 +326,8 @@ func TestAdminEvacuateUser(t *testing.T) {
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: "",
accessTokens := map[*test.User]userDevice{
aliceAdmin: {},
}
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
@ -329,7 +347,7 @@ func TestAdminEvacuateUser(t *testing.T) {
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])
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
rec := httptest.NewRecorder()
routers.DendriteAdmin.ServeHTTP(rec, req)
@ -390,8 +408,8 @@ func TestAdminMarkAsStale(t *testing.T) {
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: "",
accessTokens := map[*test.User]userDevice{
aliceAdmin: {},
}
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
@ -409,7 +427,7 @@ func TestAdminMarkAsStale(t *testing.T) {
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])
req.Header.Set("Authorization", "Bearer "+accessTokens[aliceAdmin].accessToken)
rec := httptest.NewRecorder()
routers.DendriteAdmin.ServeHTTP(rec, req)
@ -421,35 +439,3 @@ func TestAdminMarkAsStale(t *testing.T) {
}
})
}
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()
}
}

View file

@ -1,7 +1,9 @@
package clientapi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
@ -10,17 +12,596 @@ import (
"testing"
"time"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
"github.com/tidwall/gjson"
"github.com/matrix-org/dendrite/appservice"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/internal/caching"
"github.com/matrix-org/dendrite/internal/httputil"
"github.com/matrix-org/dendrite/internal/sqlutil"
"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/jetstream"
"github.com/matrix-org/dendrite/test"
"github.com/matrix-org/dendrite/test/testrig"
"github.com/matrix-org/dendrite/userapi"
uapi "github.com/matrix-org/dendrite/userapi/api"
)
type userDevice struct {
accessToken string
deviceID string
password string
}
func TestGetPutDevices(t *testing.T) {
alice := test.NewUser(t)
bob := test.NewUser(t)
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
testCases := []struct {
name string
requestUser *test.User
deviceUser *test.User
request *http.Request
wantStatusCode int
validateFunc func(t *testing.T, device userDevice, routers httputil.Routers)
}{
{
name: "can get all devices",
requestUser: alice,
request: httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices", strings.NewReader("")),
wantStatusCode: http.StatusOK,
},
{
name: "can get specific own device",
requestUser: alice,
deviceUser: alice,
request: httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices/", strings.NewReader("")),
wantStatusCode: http.StatusOK,
},
{
name: "can not get device for different user",
requestUser: alice,
deviceUser: bob,
request: httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices/", strings.NewReader("")),
wantStatusCode: http.StatusNotFound,
},
{
name: "can update own device",
requestUser: alice,
deviceUser: alice,
request: httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/devices/", strings.NewReader(`{"display_name":"my new displayname"}`)),
wantStatusCode: http.StatusOK,
validateFunc: func(t *testing.T, device userDevice, routers httputil.Routers) {
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices/"+device.deviceID, strings.NewReader(""))
req.Header.Set("Authorization", "Bearer "+device.accessToken)
rec := httptest.NewRecorder()
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "display_name").Str
if gotDisplayName != "my new displayname" {
t.Fatalf("expected displayname '%s', got '%s'", "my new displayname", gotDisplayName)
}
},
},
{
// this should return "device does not exist"
name: "can not update device for different user",
requestUser: alice,
deviceUser: bob,
request: httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/devices/", strings.NewReader(`{"display_name":"my new displayname"}`)),
wantStatusCode: http.StatusNotFound,
},
}
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)
accessTokens := map[*test.User]userDevice{
alice: {},
bob: {},
}
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dev := accessTokens[tc.requestUser]
if tc.deviceUser != nil {
tc.request = httptest.NewRequest(tc.request.Method, tc.request.RequestURI+accessTokens[tc.deviceUser].deviceID, tc.request.Body)
}
tc.request.Header.Set("Authorization", "Bearer "+dev.accessToken)
rec := httptest.NewRecorder()
routers.Client.ServeHTTP(rec, tc.request)
if rec.Code != tc.wantStatusCode {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
if tc.wantStatusCode != http.StatusOK && rec.Code != http.StatusOK {
return
}
if tc.validateFunc != nil {
tc.validateFunc(t, dev, routers)
}
})
}
})
}
// Deleting devices requires the UIA dance, so do this in a different test
func TestDeleteDevice(t *testing.T) {
alice := test.NewUser(t)
localpart, serverName, _ := gomatrixserverlib.SplitID('@', alice.ID)
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
defer closeDB()
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)
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)
accessTokens := map[*test.User]userDevice{
alice: {},
}
// create the account and an initial device
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
// create some more devices
accessToken := util.RandomString(8)
devRes := &uapi.PerformDeviceCreationResponse{}
if err := userAPI.PerformDeviceCreation(processCtx.Context(), &uapi.PerformDeviceCreationRequest{
Localpart: localpart,
ServerName: serverName,
AccessToken: accessToken,
NoDeviceListUpdate: true,
}, devRes); err != nil {
t.Fatal(err)
}
if !devRes.DeviceCreated {
t.Fatalf("failed to create device")
}
secondDeviceID := devRes.Device.ID
// initiate UIA for the second device
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/_matrix/client/v3/devices/"+secondDeviceID, strings.NewReader(""))
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected HTTP 401, got %d: %s", rec.Code, rec.Body.String())
}
// get the session ID
sessionID := gjson.GetBytes(rec.Body.Bytes(), "session").Str
// prepare UIA request body
reqBody := bytes.Buffer{}
if err := json.NewEncoder(&reqBody).Encode(map[string]interface{}{
"auth": map[string]string{
"session": sessionID,
"type": authtypes.LoginTypePassword,
"user": alice.ID,
"password": accessTokens[alice].password,
},
}); err != nil {
t.Fatal(err)
}
// copy the request body, so we can use it again for the successful delete
reqBody2 := reqBody
// do the same request again, this time with our UIA, but for a different device ID, this should fail
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodDelete, "/_matrix/client/v3/devices/"+accessTokens[alice].deviceID, &reqBody)
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("expected HTTP 403, got %d: %s", rec.Code, rec.Body.String())
}
// do the same request again, this time with our UIA, but for the correct device ID, this should be fine
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodDelete, "/_matrix/client/v3/devices/"+secondDeviceID, &reqBody2)
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
// verify devices are deleted
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices", strings.NewReader(""))
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
for _, device := range gjson.GetBytes(rec.Body.Bytes(), "devices.#.device_id").Array() {
if device.Str == secondDeviceID {
t.Fatalf("expected device %s to be deleted, but wasn't", secondDeviceID)
}
}
})
}
// Deleting devices requires the UIA dance, so do this in a different test
func TestDeleteDevices(t *testing.T) {
alice := test.NewUser(t)
localpart, serverName, _ := gomatrixserverlib.SplitID('@', alice.ID)
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
defer closeDB()
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)
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)
accessTokens := map[*test.User]userDevice{
alice: {},
}
// create the account and an initial device
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
// create some more devices
var devices []string
for i := 0; i < 10; i++ {
accessToken := util.RandomString(8)
devRes := &uapi.PerformDeviceCreationResponse{}
if err := userAPI.PerformDeviceCreation(processCtx.Context(), &uapi.PerformDeviceCreationRequest{
Localpart: localpart,
ServerName: serverName,
AccessToken: accessToken,
NoDeviceListUpdate: true,
}, devRes); err != nil {
t.Fatal(err)
}
if !devRes.DeviceCreated {
t.Fatalf("failed to create device")
}
devices = append(devices, devRes.Device.ID)
}
// initiate UIA
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/_matrix/client/v3/delete_devices", strings.NewReader(""))
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected HTTP 401, got %d: %s", rec.Code, rec.Body.String())
}
// get the session ID
sessionID := gjson.GetBytes(rec.Body.Bytes(), "session").Str
// prepare UIA request body
reqBody := bytes.Buffer{}
if err := json.NewEncoder(&reqBody).Encode(map[string]interface{}{
"auth": map[string]string{
"session": sessionID,
"type": authtypes.LoginTypePassword,
"user": alice.ID,
"password": accessTokens[alice].password,
},
"devices": devices[5:],
}); err != nil {
t.Fatal(err)
}
// do the same request again, this time with our UIA,
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/_matrix/client/v3/delete_devices", &reqBody)
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
// verify devices are deleted
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/devices", strings.NewReader(""))
req.Header.Set("Authorization", "Bearer "+accessTokens[alice].accessToken)
routers.Client.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
}
for _, device := range gjson.GetBytes(rec.Body.Bytes(), "devices.#.device_id").Array() {
for _, deletedDevice := range devices[5:] {
if device.Str == deletedDevice {
t.Fatalf("expected device %s to be deleted, but wasn't", deletedDevice)
}
}
}
})
}
func createAccessTokens(t *testing.T, accessTokens map[*test.User]userDevice, 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] = userDevice{
accessToken: gjson.GetBytes(rec.Body.Bytes(), "access_token").String(),
deviceID: gjson.GetBytes(rec.Body.Bytes(), "device_id").String(),
password: password,
}
}
}
func TestSetDisplayname(t *testing.T) {
alice := test.NewUser(t)
bob := test.NewUser(t)
notLocalUser := &test.User{ID: "@charlie:localhost", Localpart: "charlie"}
changeDisplayName := "my new display name"
testCases := []struct {
name string
user *test.User
wantOK bool
changeReq io.Reader
wantDisplayName string
}{
{
name: "invalid user",
user: &test.User{ID: "!notauser"},
},
{
name: "non-existent user",
user: &test.User{ID: "@doesnotexist:test"},
},
{
name: "non-local user is not allowed",
user: notLocalUser,
},
{
name: "existing user is allowed to change own name",
user: alice,
wantOK: true,
wantDisplayName: changeDisplayName,
},
{
name: "existing user is not allowed to change own name if name is empty",
user: bob,
wantOK: false,
wantDisplayName: "",
},
}
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
defer closeDB()
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
routers := httputil.NewRouters()
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
natsInstance := &jetstream.NATSInstance{}
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
rsAPI.SetFederationAPI(nil, nil)
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil)
asPI := appservice.NewInternalAPI(processCtx, cfg, natsInstance, userAPI, rsAPI)
AddPublicRoutes(processCtx, routers, cfg, natsInstance, base.CreateFederationClient(cfg, nil), rsAPI, asPI, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
accessTokens := map[*test.User]userDevice{
alice: {},
bob: {},
}
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
wantDisplayName := tc.user.Localpart
if tc.changeReq == nil {
tc.changeReq = strings.NewReader("")
}
// check profile after initial account creation
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID, strings.NewReader(""))
t.Logf("%s", req.URL.String())
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d", rec.Code)
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "displayname").Str; tc.wantOK && gotDisplayName != wantDisplayName {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantDisplayName, gotDisplayName)
}
// now set the new display name
wantDisplayName = tc.wantDisplayName
tc.changeReq = strings.NewReader(fmt.Sprintf(`{"displayname":"%s"}`, tc.wantDisplayName))
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/profile/"+tc.user.ID+"/displayname", tc.changeReq)
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.user].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())
}
// now only get the display name
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID+"/displayname", strings.NewReader(""))
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())
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "displayname").Str; tc.wantOK && gotDisplayName != wantDisplayName {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantDisplayName, gotDisplayName)
}
})
}
})
}
func TestSetAvatarURL(t *testing.T) {
alice := test.NewUser(t)
bob := test.NewUser(t)
notLocalUser := &test.User{ID: "@charlie:localhost", Localpart: "charlie"}
changeDisplayName := "mxc://newMXID"
testCases := []struct {
name string
user *test.User
wantOK bool
changeReq io.Reader
avatar_url string
}{
{
name: "invalid user",
user: &test.User{ID: "!notauser"},
},
{
name: "non-existent user",
user: &test.User{ID: "@doesnotexist:test"},
},
{
name: "non-local user is not allowed",
user: notLocalUser,
},
{
name: "existing user is allowed to change own avatar",
user: alice,
wantOK: true,
avatar_url: changeDisplayName,
},
{
name: "existing user is not allowed to change own avatar if avatar is empty",
user: bob,
wantOK: false,
avatar_url: "",
},
}
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
defer closeDB()
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
routers := httputil.NewRouters()
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
natsInstance := &jetstream.NATSInstance{}
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
rsAPI.SetFederationAPI(nil, nil)
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil)
asPI := appservice.NewInternalAPI(processCtx, cfg, natsInstance, userAPI, rsAPI)
AddPublicRoutes(processCtx, routers, cfg, natsInstance, base.CreateFederationClient(cfg, nil), rsAPI, asPI, nil, nil, userAPI, nil, nil, caching.DisableMetrics)
accessTokens := map[*test.User]userDevice{
alice: {},
bob: {},
}
createAccessTokens(t, accessTokens, userAPI, processCtx.Context(), routers)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
wantAvatarURL := ""
if tc.changeReq == nil {
tc.changeReq = strings.NewReader("")
}
// check profile after initial account creation
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID, strings.NewReader(""))
t.Logf("%s", req.URL.String())
routers.Client.ServeHTTP(rec, req)
if tc.wantOK && rec.Code != http.StatusOK {
t.Fatalf("expected HTTP 200, got %d", rec.Code)
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "avatar_url").Str; tc.wantOK && gotDisplayName != wantAvatarURL {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantAvatarURL, gotDisplayName)
}
// now set the new display name
wantAvatarURL = tc.avatar_url
tc.changeReq = strings.NewReader(fmt.Sprintf(`{"avatar_url":"%s"}`, tc.avatar_url))
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPut, "/_matrix/client/v3/profile/"+tc.user.ID+"/avatar_url", tc.changeReq)
req.Header.Set("Authorization", "Bearer "+accessTokens[tc.user].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())
}
// now only get the display name
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/profile/"+tc.user.ID+"/avatar_url", strings.NewReader(""))
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())
}
if gotDisplayName := gjson.GetBytes(rec.Body.Bytes(), "avatar_url").Str; tc.wantOK && gotDisplayName != wantAvatarURL {
t.Fatalf("expected displayname to be '%s', but got '%s'", wantAvatarURL, gotDisplayName)
}
})
}
})
}
func TestTyping(t *testing.T) {
alice := test.NewUser(t)
room := test.NewRoom(t, alice)
@ -41,8 +622,8 @@ func TestTyping(t *testing.T) {
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{
alice: "",
accessTokens := map[*test.User]userDevice{
alice: {},
}
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
@ -93,7 +674,7 @@ func TestTyping(t *testing.T) {
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])
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())
@ -126,9 +707,9 @@ func TestMembership(t *testing.T) {
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{
alice: "",
bob: "",
accessTokens := map[*test.User]userDevice{
alice: {},
bob: {},
}
createAccessTokens(t, accessTokens, userAPI, ctx, routers)
@ -299,7 +880,7 @@ func TestMembership(t *testing.T) {
tc.asUser = alice
}
rec := httptest.NewRecorder()
tc.request.Header.Set("Authorization", "Bearer "+accessTokens[tc.asUser])
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())

View file

@ -33,7 +33,7 @@ func Deactivate(
return *errRes
}
localpart, _, err := gomatrixserverlib.SplitID('@', login.Username())
localpart, serverName, err := gomatrixserverlib.SplitID('@', login.Username())
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
return jsonerror.InternalServerError()
@ -42,6 +42,7 @@ func Deactivate(
var res api.PerformAccountDeactivationResponse
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
Localpart: localpart,
ServerName: serverName,
}, &res)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")

View file

@ -15,6 +15,7 @@
package routing
import (
"encoding/json"
"io"
"net"
"net/http"
@ -146,12 +147,6 @@ func UpdateDeviceByID(
JSON: jsonerror.Forbidden("device does not exist"),
}
}
if performRes.Forbidden {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("device not owned by current user"),
}
}
return util.JSONResponse{
Code: http.StatusOK,
@ -189,7 +184,7 @@ func DeleteDeviceById(
if dev != deviceID {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("session & device mismatch"),
JSON: jsonerror.Forbidden("session and device mismatch"),
}
}
}
@ -242,16 +237,37 @@ func DeleteDeviceById(
// DeleteDevices handles POST requests to /delete_devices
func DeleteDevices(
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.ClientUserAPI, device *api.Device,
) util.JSONResponse {
ctx := req.Context()
payload := devicesDeleteJSON{}
if resErr := httputil.UnmarshalJSONRequest(req, &payload); resErr != nil {
return *resErr
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
}
}
defer req.Body.Close() // nolint:errcheck
// initiate UIA
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
if errRes != nil {
return *errRes
}
defer req.Body.Close() // nolint: errcheck
if login.Username() != device.UserID {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("unable to delete devices for other user"),
}
}
payload := devicesDeleteJSON{}
if err = json.Unmarshal(bodyBytes, &payload); err != nil {
util.GetLogger(ctx).WithError(err).Error("unable to unmarshal device deletion request")
return jsonerror.InternalServerError()
}
var res api.PerformDeviceDeletionResponse
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{

View file

@ -18,6 +18,7 @@ import (
"net/http"
"time"
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
@ -61,21 +62,19 @@ func JoinRoomByIDOrAlias(
// Work out our localpart for the client profile request.
// Request our profile content to populate the request content with.
res := &api.QueryProfileResponse{}
err := profileAPI.QueryProfile(req.Context(), &api.QueryProfileRequest{UserID: device.UserID}, res)
if err != nil || !res.UserExists {
if !res.UserExists {
profile, err := profileAPI.QueryProfile(req.Context(), device.UserID)
switch err {
case nil:
joinReq.Content["displayname"] = profile.DisplayName
joinReq.Content["avatar_url"] = profile.AvatarURL
case appserviceAPI.ErrProfileNotExists:
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: jsonerror.Unknown("Unable to query user profile, no profile found."),
}
}
util.GetLogger(req.Context()).WithError(err).Error("UserProfileAPI.QueryProfile failed")
} else {
joinReq.Content["displayname"] = res.DisplayName
joinReq.Content["avatar_url"] = res.AvatarURL
default:
}
// Ask the roomserver to perform the join.

View file

@ -36,14 +36,14 @@ import (
// GetProfile implements GET /profile/{userID}
func GetProfile(
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string,
asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse {
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
if err != nil {
if err == eventutil.ErrProfileNoExists {
if err == appserviceAPI.ErrProfileNotExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
@ -56,7 +56,7 @@ func GetProfile(
return util.JSONResponse{
Code: http.StatusOK,
JSON: eventutil.ProfileResponse{
JSON: eventutil.UserProfile{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
},
@ -65,34 +65,28 @@ func GetProfile(
// GetAvatarURL implements GET /profile/{userID}/avatar_url
func GetAvatarURL(
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse {
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
if err != nil {
if err == eventutil.ErrProfileNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
}
}
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
return jsonerror.InternalServerError()
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
p, ok := profile.JSON.(eventutil.UserProfile)
// not a profile response, so most likely an error, return that
if !ok {
return profile
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: eventutil.AvatarURL{
AvatarURL: profile.AvatarURL,
JSON: eventutil.UserProfile{
AvatarURL: p.AvatarURL,
},
}
}
// SetAvatarURL implements PUT /profile/{userID}/avatar_url
func SetAvatarURL(
req *http.Request, profileAPI userapi.ClientUserAPI,
req *http.Request, profileAPI userapi.ProfileAPI,
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
) util.JSONResponse {
if userID != device.UserID {
@ -102,7 +96,7 @@ func SetAvatarURL(
}
}
var r eventutil.AvatarURL
var r eventutil.UserProfile
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr
}
@ -134,24 +128,20 @@ func SetAvatarURL(
}
}
setRes := &userapi.PerformSetAvatarURLResponse{}
if err = profileAPI.SetAvatarURL(req.Context(), &userapi.PerformSetAvatarURLRequest{
Localpart: localpart,
ServerName: domain,
AvatarURL: r.AvatarURL,
}, setRes); err != nil {
profile, changed, err := profileAPI.SetAvatarURL(req.Context(), localpart, domain, r.AvatarURL)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetAvatarURL failed")
return jsonerror.InternalServerError()
}
// No need to build new membership events, since nothing changed
if !setRes.Changed {
if !changed {
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}
response, err := updateProfile(req.Context(), rsAPI, device, setRes.Profile, userID, cfg, evTime)
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, cfg, evTime)
if err != nil {
return response
}
@ -164,34 +154,28 @@ func SetAvatarURL(
// GetDisplayName implements GET /profile/{userID}/displayname
func GetDisplayName(
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
) util.JSONResponse {
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
if err != nil {
if err == eventutil.ErrProfileNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
}
}
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
return jsonerror.InternalServerError()
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
p, ok := profile.JSON.(eventutil.UserProfile)
// not a profile response, so most likely an error, return that
if !ok {
return profile
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: eventutil.DisplayName{
DisplayName: profile.DisplayName,
JSON: eventutil.UserProfile{
DisplayName: p.DisplayName,
},
}
}
// SetDisplayName implements PUT /profile/{userID}/displayname
func SetDisplayName(
req *http.Request, profileAPI userapi.ClientUserAPI,
req *http.Request, profileAPI userapi.ProfileAPI,
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
) util.JSONResponse {
if userID != device.UserID {
@ -201,7 +185,7 @@ func SetDisplayName(
}
}
var r eventutil.DisplayName
var r eventutil.UserProfile
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
return *resErr
}
@ -233,25 +217,20 @@ func SetDisplayName(
}
}
profileRes := &userapi.PerformUpdateDisplayNameResponse{}
err = profileAPI.SetDisplayName(req.Context(), &userapi.PerformUpdateDisplayNameRequest{
Localpart: localpart,
ServerName: domain,
DisplayName: r.DisplayName,
}, profileRes)
profile, changed, err := profileAPI.SetDisplayName(req.Context(), localpart, domain, r.DisplayName)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetDisplayName failed")
return jsonerror.InternalServerError()
}
// No need to build new membership events, since nothing changed
if !profileRes.Changed {
if !changed {
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}
response, err := updateProfile(req.Context(), rsAPI, device, profileRes.Profile, userID, cfg, evTime)
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, cfg, evTime)
if err != nil {
return response
}
@ -308,9 +287,9 @@ func updateProfile(
// getProfile gets the full profile of a user by querying the database or a
// remote homeserver.
// Returns an error when something goes wrong or specifically
// eventutil.ErrProfileNoExists when the profile doesn't exist.
// eventutil.ErrProfileNotExists when the profile doesn't exist.
func getProfile(
ctx context.Context, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
ctx context.Context, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
userID string,
asAPI appserviceAPI.AppServiceInternalAPI,
federation *gomatrixserverlib.FederationClient,
@ -325,7 +304,7 @@ func getProfile(
if fedErr != nil {
if x, ok := fedErr.(gomatrix.HTTPError); ok {
if x.Code == http.StatusNotFound {
return nil, eventutil.ErrProfileNoExists
return nil, appserviceAPI.ErrProfileNotExists
}
}

View file

@ -888,13 +888,7 @@ func completeRegistration(
}
if displayName != "" {
nameReq := userapi.PerformUpdateDisplayNameRequest{
Localpart: username,
ServerName: serverName,
DisplayName: displayName,
}
var nameRes userapi.PerformUpdateDisplayNameResponse
err = userAPI.SetDisplayName(ctx, &nameReq, &nameRes)
_, _, err = userAPI.SetDisplayName(ctx, username, serverName, displayName)
if err != nil {
return util.JSONResponse{
Code: http.StatusInternalServerError,

View file

@ -611,11 +611,9 @@ func TestRegisterUserWithDisplayName(t *testing.T) {
assert.Equal(t, http.StatusOK, response.Code)
req := api.QueryProfileRequest{UserID: "@user:server"}
var res api.QueryProfileResponse
err := userAPI.QueryProfile(processCtx.Context(), &req, &res)
profile, err := userAPI.QueryProfile(processCtx.Context(), "@user:server")
assert.NoError(t, err)
assert.Equal(t, expectedDisplayName, res.DisplayName)
assert.Equal(t, expectedDisplayName, profile.DisplayName)
})
}
@ -662,10 +660,8 @@ func TestRegisterAdminUsingSharedSecret(t *testing.T) {
)
assert.Equal(t, http.StatusOK, response.Code)
profilReq := api.QueryProfileRequest{UserID: "@alice:server"}
var profileRes api.QueryProfileResponse
err = userAPI.QueryProfile(processCtx.Context(), &profilReq, &profileRes)
profile, err := userAPI.QueryProfile(processCtx.Context(), "@alice:server")
assert.NoError(t, err)
assert.Equal(t, expectedDisplayName, profileRes.DisplayName)
assert.Equal(t, expectedDisplayName, profile.DisplayName)
})
}

View file

@ -18,7 +18,6 @@ import (
"context"
"net/http"
"strings"
"sync"
"github.com/gorilla/mux"
userapi "github.com/matrix-org/dendrite/userapi/api"
@ -198,18 +197,13 @@ func Setup(
// server notifications
if cfg.Matrix.ServerNotices.Enabled {
logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice")
var serverNotificationSender *userapi.Device
var err error
notificationSenderOnce := &sync.Once{}
synapseAdminRouter.Handle("/admin/v1/send_server_notice/{txnID}",
httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
notificationSenderOnce.Do(func() {
serverNotificationSender, err = getSenderDevice(context.Background(), rsAPI, userAPI, cfg)
serverNotificationSender, err := getSenderDevice(context.Background(), rsAPI, userAPI, cfg)
if err != nil {
logrus.WithError(err).Fatal("unable to get account for sending sending server notices")
}
})
synapseAdminRouter.Handle("/admin/v1/send_server_notice/{txnID}",
httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
// not specced, but ensure we're rate limiting requests to this endpoint
if r := rateLimits.Limit(req, device); r != nil {
return *r
@ -231,12 +225,6 @@ func Setup(
synapseAdminRouter.Handle("/admin/v1/send_server_notice",
httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
notificationSenderOnce.Do(func() {
serverNotificationSender, err = getSenderDevice(context.Background(), rsAPI, userAPI, cfg)
if err != nil {
logrus.WithError(err).Fatal("unable to get account for sending sending server notices")
}
})
// not specced, but ensure we're rate limiting requests to this endpoint
if r := rateLimits.Limit(req, device); r != nil {
return *r
@ -1115,7 +1103,7 @@ func Setup(
v3mux.Handle("/delete_devices",
httputil.MakeAuthAPI("delete_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
return DeleteDevices(req, userAPI, device)
return DeleteDevices(req, userInteractiveAuth, userAPI, device)
}),
).Methods(http.MethodPost, http.MethodOptions)

View file

@ -295,30 +295,28 @@ func getSenderDevice(
}
// Set the avatarurl for the user
avatarRes := &userapi.PerformSetAvatarURLResponse{}
if err = userAPI.SetAvatarURL(ctx, &userapi.PerformSetAvatarURLRequest{
Localpart: cfg.Matrix.ServerNotices.LocalPart,
ServerName: cfg.Matrix.ServerName,
AvatarURL: cfg.Matrix.ServerNotices.AvatarURL,
}, avatarRes); err != nil {
profile, avatarChanged, err := userAPI.SetAvatarURL(ctx,
cfg.Matrix.ServerNotices.LocalPart,
cfg.Matrix.ServerName,
cfg.Matrix.ServerNotices.AvatarURL,
)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("userAPI.SetAvatarURL failed")
return nil, err
}
profile := avatarRes.Profile
// Set the displayname for the user
displayNameRes := &userapi.PerformUpdateDisplayNameResponse{}
if err = userAPI.SetDisplayName(ctx, &userapi.PerformUpdateDisplayNameRequest{
Localpart: cfg.Matrix.ServerNotices.LocalPart,
ServerName: cfg.Matrix.ServerName,
DisplayName: cfg.Matrix.ServerNotices.DisplayName,
}, displayNameRes); err != nil {
_, displayNameChanged, err := userAPI.SetDisplayName(ctx,
cfg.Matrix.ServerNotices.LocalPart,
cfg.Matrix.ServerName,
cfg.Matrix.ServerNotices.DisplayName,
)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("userAPI.SetDisplayName failed")
return nil, err
}
if displayNameRes.Changed {
if displayNameChanged {
profile.DisplayName = cfg.Matrix.ServerNotices.DisplayName
}
@ -334,7 +332,7 @@ func getSenderDevice(
// We've got an existing account, return the first device of it
if len(deviceRes.Devices) > 0 {
// If there were changes to the profile, create a new membership event
if displayNameRes.Changed || avatarRes.Changed {
if displayNameChanged || avatarChanged {
_, err = updateProfile(ctx, rsAPI, &deviceRes.Devices[0], profile, accRes.Account.UserID, cfg, time.Now())
if err != nil {
return nil, err

View file

@ -209,24 +209,17 @@ func queryIDServerStoreInvite(
body *MembershipRequest, roomID string,
) (*idServerStoreInviteResponse, error) {
// Retrieve the sender's profile to get their display name
localpart, serverName, err := gomatrixserverlib.SplitID('@', device.UserID)
_, serverName, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
return nil, err
}
var profile *authtypes.Profile
if cfg.Matrix.IsLocalServerName(serverName) {
res := &userapi.QueryProfileResponse{}
err = userAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: device.UserID}, res)
profile, err = userAPI.QueryProfile(ctx, device.UserID)
if err != nil {
return nil, err
}
profile = &authtypes.Profile{
Localpart: localpart,
DisplayName: res.DisplayName,
AvatarURL: res.AvatarURL,
}
} else {
profile = &authtypes.Profile{}
}

View file

@ -18,7 +18,7 @@ Mostly, although there are still bugs and missing features. If you are a confide
## Is Dendrite feature-complete?
No, although a good portion of the Matrix specification has been implemented. Mostly missing are client features - see the [readme](../README.md) at the root of the repository for more information.
No, although a good portion of the Matrix specification has been implemented. Mostly missing are client features - see the [readme](https://github.com/matrix-org/dendrite/blob/main/README.md) at the root of the repository for more information.
## Why doesn't Dendrite have "x" yet?

View file

@ -50,10 +50,7 @@ func GetProfile(
}
}
var profileRes userapi.QueryProfileResponse
err = userAPI.QueryProfile(httpReq.Context(), &userapi.QueryProfileRequest{
UserID: userID,
}, &profileRes)
profile, err := userAPI.QueryProfile(httpReq.Context(), userID)
if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("userAPI.QueryProfile failed")
return jsonerror.InternalServerError()
@ -65,21 +62,21 @@ func GetProfile(
if field != "" {
switch field {
case "displayname":
res = eventutil.DisplayName{
DisplayName: profileRes.DisplayName,
res = eventutil.UserProfile{
DisplayName: profile.DisplayName,
}
case "avatar_url":
res = eventutil.AvatarURL{
AvatarURL: profileRes.AvatarURL,
res = eventutil.UserProfile{
AvatarURL: profile.AvatarURL,
}
default:
code = http.StatusBadRequest
res = jsonerror.InvalidArgumentValue("The request body did not contain an allowed value of argument 'field'. Allowed values are either: 'avatar_url', 'displayname'.")
}
} else {
res = eventutil.ProfileResponse{
AvatarURL: profileRes.AvatarURL,
DisplayName: profileRes.DisplayName,
res = eventutil.UserProfile{
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
}
}

View file

@ -23,6 +23,7 @@ import (
"testing"
"github.com/gorilla/mux"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
fedAPI "github.com/matrix-org/dendrite/federationapi"
fedInternal "github.com/matrix-org/dendrite/federationapi/internal"
@ -43,8 +44,8 @@ type fakeUserAPI struct {
userAPI.FederationUserAPI
}
func (u *fakeUserAPI) QueryProfile(ctx context.Context, req *userAPI.QueryProfileRequest, res *userAPI.QueryProfileResponse) error {
return nil
func (u *fakeUserAPI) QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error) {
return &authtypes.Profile{}, nil
}
func TestHandleQueryProfile(t *testing.T) {

View file

@ -256,17 +256,14 @@ func createInviteFrom3PIDInvite(
StateKey: &inv.MXID,
}
var res userapi.QueryProfileResponse
err = userAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{
UserID: inv.MXID,
}, &res)
profile, err := userAPI.QueryProfile(ctx, inv.MXID)
if err != nil {
return nil, err
}
content := gomatrixserverlib.MemberContent{
AvatarURL: res.AvatarURL,
DisplayName: res.DisplayName,
AvatarURL: profile.AvatarURL,
DisplayName: profile.DisplayName,
Membership: gomatrixserverlib.Invite,
ThirdPartyInvite: &gomatrixserverlib.MemberThirdPartyInvite{
Signed: inv.Signed,

View file

@ -1,10 +1,15 @@
{{ define "chart.monitoringSection" }}
## Monitoring
[![Grafana Dashboard](https://grafana.com/api/dashboards/13916/images/9894/image)](https://grafana.com/grafana/dashboards/13916-dendrite/)
![Grafana Dashboard](grafana_dashboards/dendrite-rev2.png)
* Works well with [Prometheus Operator](https://prometheus-operator.dev/) ([Helmchart](https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack)) and their setup of [Grafana](https://grafana.com/grafana/), by enabling the following values:
```yaml
dendrite_config:
global:
metrics:
enabled: true
prometheus:
servicemonitor:
enabled: true

View file

@ -1,6 +1,6 @@
apiVersion: v2
name: dendrite
version: "0.12.1"
version: "0.12.2"
appVersion: "0.12.0"
description: Dendrite Matrix Homeserver
type: application

View file

@ -1,6 +1,7 @@
# dendrite
![Version: 0.12.0](https://img.shields.io/badge/Version-0.12.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.12.0](https://img.shields.io/badge/AppVersion-0.12.0-informational?style=flat-square)
![Version: 0.12.2](https://img.shields.io/badge/Version-0.12.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.12.0](https://img.shields.io/badge/AppVersion-0.12.0-informational?style=flat-square)
Dendrite Matrix Homeserver
Status: **NOT PRODUCTION READY**
@ -54,6 +55,11 @@ Create a folder `appservices` and place your configurations in there. The confi
| persistence.media.capacity | string | `"1Gi"` | PVC Storage Request for the media volume |
| persistence.search.existingClaim | string | `""` | Use an existing volume claim for the fulltext search index |
| persistence.search.capacity | string | `"1Gi"` | PVC Storage Request for the search volume |
| extraVolumes | list | `[]` | Add additional volumes to the Dendrite Pod |
| extraVolumeMounts | list | `[]` | Configure additional mount points volumes in the Dendrite Pod |
| strategy.type | string | `"RollingUpdate"` | Strategy to use for rolling updates (e.g. Recreate, RollingUpdate) If you are using ReadWriteOnce volumes, you should probably use Recreate |
| strategy.rollingUpdate.maxUnavailable | string | `"25%"` | Maximum number of pods that can be unavailable during the update process |
| strategy.rollingUpdate.maxSurge | string | `"25%"` | Maximum number of pods that can be scheduled above the desired number of pods |
| dendrite_config.version | int | `2` | |
| dendrite_config.global.server_name | string | `""` | **REQUIRED** Servername for this Dendrite deployment. |
| dendrite_config.global.private_key | string | `"/etc/dendrite/secrets/signing.key"` | The private key to use. (**NOTE**: This is overriden in Helm) |
@ -157,10 +163,15 @@ Create a folder `appservices` and place your configurations in there. The confi
## Monitoring
[![Grafana Dashboard](https://grafana.com/api/dashboards/13916/images/9894/image)](https://grafana.com/grafana/dashboards/13916-dendrite/)
![Grafana Dashboard](grafana_dashboards/dendrite-rev2.png)
* Works well with [Prometheus Operator](https://prometheus-operator.dev/) ([Helmchart](https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack)) and their setup of [Grafana](https://grafana.com/grafana/), by enabling the following values:
```yaml
dendrite_config:
global:
metrics:
enabled: true
prometheus:
servicemonitor:
enabled: true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,479 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Dendrite dashboard from https://github.com/matrix-org/dendrite/",
"editable": true,
"fiscalYearStartMonth": 0,
"gnetId": 13916,
"graphTooltip": 0,
"id": 60,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"targets": [
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"refId": "A"
}
],
"title": "Overview",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "Total number of registered users",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 7,
"x": 0,
"y": 1
},
"id": 20,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "9.3.6",
"targets": [
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum(dendrite_clientapi_reg_users_total{namespace=~\"$namespace\",service=~\"$service\"}) by (namespace,service)",
"instant": false,
"interval": "",
"legendFormat": "{{namespace}}: {{service}}",
"refId": "A"
}
],
"title": "Registerd Users",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "The number of sync requests that are active right now and are waiting to be woken by a notifier",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ops"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 17,
"x": 7,
"y": 1
},
"id": 6,
"options": {
"legend": {
"calcs": [
"mean",
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.3.6",
"targets": [
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(dendrite_syncapi_active_sync_requests{namespace=~\"$namespace\",service=~\"$service\"}[$__rate_interval]))by (namspace,service)",
"hide": false,
"interval": "",
"legendFormat": "active: {{namspace}} - {{service}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(dendrite_syncapi_waiting_sync_requests{namespace=~\"$namespace\",service=~\"$service\"}[$__rate_interval]))by (namespace,service)",
"hide": false,
"interval": "",
"legendFormat": "waiting: {{namspace}} - {{service}}",
"range": true,
"refId": "B"
}
],
"title": "Sync API",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 9
},
"id": 8,
"panels": [],
"targets": [
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"refId": "A"
}
],
"title": "Federation",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "Collection of queues for sending transactions to other matrix servers",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 10
},
"id": 10,
"options": {
"legend": {
"calcs": [
"mean",
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.3.6",
"targets": [
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "dendrite_federationapi_destination_queues_running{namespace=~\"$namespace\",service=~\"$service\"}",
"interval": "",
"legendFormat": "Queue Running: {{namespace}}-{{service}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "dendrite_federationapi_destination_queues_total{namespace=~\"$namespace\",service=~\"$service\"}",
"hide": false,
"interval": "",
"legendFormat": "Queue Total: {{namespace}}-{{service}}",
"range": true,
"refId": "B"
},
{
"datasource": {
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "dendrite_federationapi_destination_queues_backing_off{namespace=~\"$namespace\",service=~\"$service\"}",
"hide": false,
"interval": "",
"legendFormat": "Backing Off: {{namespace}}-{{service}}",
"range": true,
"refId": "C"
}
],
"title": "Federation Sender Destination",
"type": "timeseries"
}
],
"refresh": "10s",
"schemaVersion": 37,
"style": "dark",
"tags": [
"matrix",
"dendrite"
],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"label": "datasource",
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
},
{
"current": {
"selected": true,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"definition": "label_values(dendrite_syncapi_active_sync_requests, namespace)",
"hide": 0,
"includeAll": true,
"multi": true,
"name": "namespace",
"options": [],
"query": {
"query": "label_values(dendrite_syncapi_active_sync_requests, namespace)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"current": {
"selected": false,
"text": "All",
"value": "$__all"
},
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"definition": "label_values(dendrite_syncapi_active_sync_requests{namespace=~\"$namespace\"}, service)",
"hide": 0,
"includeAll": true,
"multi": true,
"name": "service",
"options": [],
"query": {
"query": "label_values(dendrite_syncapi_active_sync_requests{namespace=~\"$namespace\"}, service)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-3h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Dendrite",
"uid": "RoRt1jEGz",
"version": 1,
"weekStart": ""
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View file

@ -12,6 +12,13 @@ spec:
matchLabels:
{{- include "dendrite.selectorLabels" . | nindent 6 }}
replicas: 1
strategy:
type: {{ $.Values.strategy.type }}
{{- if eq $.Values.strategy.type "RollingUpdate" }}
rollingUpdate:
maxSurge: {{ $.Values.strategy.rollingUpdate.maxSurge }}
maxUnavailable: {{ $.Values.strategy.rollingUpdate.maxUnavailable }}
{{- end }}
template:
metadata:
labels:
@ -40,6 +47,9 @@ spec:
- name: {{ include "dendrite.fullname" . }}-search
persistentVolumeClaim:
claimName: {{ default (print ( include "dendrite.fullname" . ) "-search-pvc") $.Values.persistence.search.existingClaim | quote }}
{{- with .Values.extraVolumes }}
{{ . | toYaml | nindent 6 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- include "image.name" . | nindent 8 }}
@ -73,6 +83,9 @@ spec:
name: {{ include "dendrite.fullname" . }}-jetstream
- mountPath: {{ .Values.dendrite_config.sync_api.search.index_path }}
name: {{ include "dendrite.fullname" . }}-search
{{- with .Values.extraVolumeMounts }}
{{ . | toYaml | nindent 8 }}
{{- end }}
livenessProbe:
initialDelaySeconds: 10
periodSeconds: 10

View file

@ -43,6 +43,30 @@ persistence:
# -- PVC Storage Request for the search volume
capacity: "1Gi"
# -- Add additional volumes to the Dendrite Pod
extraVolumes: []
# ex.
# - name: extra-config
# secret:
# secretName: extra-config
# -- Configure additional mount points volumes in the Dendrite Pod
extraVolumeMounts: []
# ex.
# - mountPath: /etc/dendrite/extra-config
# name: extra-config
strategy:
# -- Strategy to use for rolling updates (e.g. Recreate, RollingUpdate)
# If you are using ReadWriteOnce volumes, you should probably use Recreate
type: RollingUpdate
rollingUpdate:
# -- Maximum number of pods that can be unavailable during the update process
maxUnavailable: 25%
# -- Maximum number of pods that can be scheduled above the desired number of pods
maxSurge: 25%
dendrite_config:
version: 2
global:

View file

@ -15,16 +15,11 @@
package eventutil
import (
"errors"
"strconv"
"github.com/matrix-org/dendrite/syncapi/types"
)
// ErrProfileNoExists is returned when trying to lookup a user's profile that
// doesn't exist locally.
var ErrProfileNoExists = errors.New("no known profile for given user ID")
// AccountData represents account data sent from the client API server to the
// sync API server
type AccountData struct {
@ -56,20 +51,10 @@ type NotificationData struct {
UnreadNotificationCount int `json:"unread_notification_count"`
}
// ProfileResponse is a struct containing all known user profile data
type ProfileResponse struct {
AvatarURL string `json:"avatar_url"`
DisplayName string `json:"displayname"`
}
// AvatarURL is a struct containing only the URL to a user's avatar
type AvatarURL struct {
AvatarURL string `json:"avatar_url"`
}
// DisplayName is a struct containing only a user's display name
type DisplayName struct {
DisplayName string `json:"displayname"`
// UserProfile is a struct containing all known user profile data
type UserProfile struct {
AvatarURL string `json:"avatar_url,omitempty"`
DisplayName string `json:"displayname,omitempty"`
}
// WeakBoolean is a type that will Unmarshal to true or false even if the encoded

View file

@ -17,6 +17,7 @@ package test
import (
"crypto/ed25519"
"fmt"
"strconv"
"sync/atomic"
"testing"
@ -47,6 +48,7 @@ var (
type User struct {
ID string
Localpart string
AccountType api.AccountType
// key ID and private key of the server who has this user, if known.
keyID gomatrixserverlib.KeyID
@ -81,6 +83,7 @@ func NewUser(t *testing.T, opts ...UserOpt) *User {
WithSigningServer(serverName, keyID, privateKey)(&u)
}
u.ID = fmt.Sprintf("@%d:%s", counter, u.srvName)
u.Localpart = strconv.Itoa(int(counter))
t.Logf("NewUser: created user %s", u.ID)
return &u
}

View file

@ -58,7 +58,7 @@ type MediaUserAPI interface {
type FederationUserAPI interface {
UploadDeviceKeysAPI
QueryOpenIDToken(ctx context.Context, req *QueryOpenIDTokenRequest, res *QueryOpenIDTokenResponse) error
QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error
QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error)
QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error
QueryKeys(ctx context.Context, req *QueryKeysRequest, res *QueryKeysResponse) error
QuerySignatures(ctx context.Context, req *QuerySignaturesRequest, res *QuerySignaturesResponse) error
@ -83,9 +83,9 @@ type ClientUserAPI interface {
LoginTokenInternalAPI
UserLoginAPI
ClientKeyAPI
ProfileAPI
QueryNumericLocalpart(ctx context.Context, req *QueryNumericLocalpartRequest, res *QueryNumericLocalpartResponse) error
QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error
QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error
QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error
QueryPushers(ctx context.Context, req *QueryPushersRequest, res *QueryPushersResponse) error
QueryPushRules(ctx context.Context, req *QueryPushRulesRequest, res *QueryPushRulesResponse) error
@ -100,8 +100,6 @@ type ClientUserAPI interface {
PerformPushRulesPut(ctx context.Context, req *PerformPushRulesPutRequest, res *struct{}) error
PerformAccountDeactivation(ctx context.Context, req *PerformAccountDeactivationRequest, res *PerformAccountDeactivationResponse) error
PerformOpenIDTokenCreation(ctx context.Context, req *PerformOpenIDTokenCreationRequest, res *PerformOpenIDTokenCreationResponse) error
SetAvatarURL(ctx context.Context, req *PerformSetAvatarURLRequest, res *PerformSetAvatarURLResponse) error
SetDisplayName(ctx context.Context, req *PerformUpdateDisplayNameRequest, res *PerformUpdateDisplayNameResponse) error
QueryNotifications(ctx context.Context, req *QueryNotificationsRequest, res *QueryNotificationsResponse) error
InputAccountData(ctx context.Context, req *InputAccountDataRequest, res *InputAccountDataResponse) error
PerformKeyBackup(ctx context.Context, req *PerformKeyBackupRequest, res *PerformKeyBackupResponse) error
@ -113,6 +111,12 @@ type ClientUserAPI interface {
PerformSaveThreePIDAssociation(ctx context.Context, req *PerformSaveThreePIDAssociationRequest, res *struct{}) error
}
type ProfileAPI interface {
QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error)
SetAvatarURL(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, avatarURL string) (*authtypes.Profile, bool, error)
SetDisplayName(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, displayName string) (*authtypes.Profile, bool, error)
}
// custom api functions required by pinecone / p2p demos
type QuerySearchProfilesAPI interface {
QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error
@ -224,7 +228,6 @@ type PerformDeviceUpdateRequest struct {
}
type PerformDeviceUpdateResponse struct {
DeviceExists bool
Forbidden bool
}
type PerformDeviceDeletionRequest struct {
@ -291,22 +294,6 @@ type QueryDevicesResponse struct {
Devices []Device
}
// QueryProfileRequest is the request for QueryProfile
type QueryProfileRequest struct {
// The user ID to query
UserID string
}
// QueryProfileResponse is the response for QueryProfile
type QueryProfileResponse struct {
// True if the user exists. Querying for a profile does not create them.
UserExists bool
// The current display name if set.
DisplayName string
// The current avatar URL if set.
AvatarURL string
}
// QuerySearchProfilesRequest is the request for QueryProfile
type QuerySearchProfilesRequest struct {
// The search string to match
@ -601,16 +588,6 @@ type Notification struct {
TS gomatrixserverlib.Timestamp `json:"ts"` // Required.
}
type PerformSetAvatarURLRequest struct {
Localpart string
ServerName gomatrixserverlib.ServerName
AvatarURL string
}
type PerformSetAvatarURLResponse struct {
Profile *authtypes.Profile `json:"profile"`
Changed bool `json:"changed"`
}
type QueryNumericLocalpartRequest struct {
ServerName gomatrixserverlib.ServerName
}
@ -639,17 +616,6 @@ type QueryAccountByPasswordResponse struct {
Exists bool
}
type PerformUpdateDisplayNameRequest struct {
Localpart string
ServerName gomatrixserverlib.ServerName
DisplayName string
}
type PerformUpdateDisplayNameResponse struct {
Profile *authtypes.Profile `json:"profile"`
Changed bool `json:"changed"`
}
type QueryLocalpartForThreePIDRequest struct {
ThreePID, Medium string
}

View file

@ -23,6 +23,8 @@ import (
"strconv"
"time"
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
fedsenderapi "github.com/matrix-org/dendrite/federationapi/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
@ -386,11 +388,6 @@ func (a *UserInternalAPI) PerformDeviceUpdate(ctx context.Context, req *api.Perf
}
res.DeviceExists = true
if dev.UserID != req.RequestingUserID {
res.Forbidden = true
return nil
}
err = a.DB.UpdateDevice(ctx, localpart, domain, req.DeviceID, req.DisplayName)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("deviceDB.UpdateDevice failed")
@ -423,25 +420,26 @@ func (a *UserInternalAPI) PerformDeviceUpdate(ctx context.Context, req *api.Perf
return nil
}
func (a *UserInternalAPI) QueryProfile(ctx context.Context, req *api.QueryProfileRequest, res *api.QueryProfileResponse) error {
local, domain, err := gomatrixserverlib.SplitID('@', req.UserID)
var (
ErrIsRemoteServer = errors.New("cannot query profile of remote users")
)
func (a *UserInternalAPI) QueryProfile(ctx context.Context, userID string) (*authtypes.Profile, error) {
local, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return err
return nil, err
}
if !a.Config.Matrix.IsLocalServerName(domain) {
return fmt.Errorf("cannot query profile of remote users (server name %s)", domain)
return nil, ErrIsRemoteServer
}
prof, err := a.DB.GetProfileByLocalpart(ctx, local, domain)
if err != nil {
if err == sql.ErrNoRows {
return nil
return nil, appserviceAPI.ErrProfileNotExists
}
return err
return nil, err
}
res.UserExists = true
res.AvatarURL = prof.AvatarURL
res.DisplayName = prof.DisplayName
return nil
return prof, nil
}
func (a *UserInternalAPI) QuerySearchProfiles(ctx context.Context, req *api.QuerySearchProfilesRequest, res *api.QuerySearchProfilesResponse) error {
@ -906,11 +904,8 @@ func (a *UserInternalAPI) QueryPushRules(ctx context.Context, req *api.QueryPush
return nil
}
func (a *UserInternalAPI) SetAvatarURL(ctx context.Context, req *api.PerformSetAvatarURLRequest, res *api.PerformSetAvatarURLResponse) error {
profile, changed, err := a.DB.SetAvatarURL(ctx, req.Localpart, req.ServerName, req.AvatarURL)
res.Profile = profile
res.Changed = changed
return err
func (a *UserInternalAPI) SetAvatarURL(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, avatarURL string) (*authtypes.Profile, bool, error) {
return a.DB.SetAvatarURL(ctx, localpart, serverName, avatarURL)
}
func (a *UserInternalAPI) QueryNumericLocalpart(ctx context.Context, req *api.QueryNumericLocalpartRequest, res *api.QueryNumericLocalpartResponse) error {
@ -944,11 +939,8 @@ func (a *UserInternalAPI) QueryAccountByPassword(ctx context.Context, req *api.Q
}
}
func (a *UserInternalAPI) SetDisplayName(ctx context.Context, req *api.PerformUpdateDisplayNameRequest, res *api.PerformUpdateDisplayNameResponse) error {
profile, changed, err := a.DB.SetDisplayName(ctx, req.Localpart, req.ServerName, req.DisplayName)
res.Profile = profile
res.Changed = changed
return err
func (a *UserInternalAPI) SetDisplayName(ctx context.Context, localpart string, serverName gomatrixserverlib.ServerName, displayName string) (*authtypes.Profile, bool, error) {
return a.DB.SetDisplayName(ctx, localpart, serverName, displayName)
}
func (a *UserInternalAPI) QueryLocalpartForThreePID(ctx context.Context, req *api.QueryLocalpartForThreePIDRequest, res *api.QueryLocalpartForThreePIDResponse) error {

View file

@ -22,6 +22,8 @@ import (
"testing"
"time"
api2 "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/userapi/producers"
"github.com/matrix-org/gomatrixserverlib"
@ -111,33 +113,26 @@ func TestQueryProfile(t *testing.T) {
aliceDisplayName := "Alice"
testCases := []struct {
req api.QueryProfileRequest
wantRes api.QueryProfileResponse
userID string
wantRes *authtypes.Profile
wantErr error
}{
{
req: api.QueryProfileRequest{
UserID: fmt.Sprintf("@alice:%s", serverName),
},
wantRes: api.QueryProfileResponse{
UserExists: true,
AvatarURL: aliceAvatarURL,
userID: fmt.Sprintf("@alice:%s", serverName),
wantRes: &authtypes.Profile{
Localpart: "alice",
DisplayName: aliceDisplayName,
AvatarURL: aliceAvatarURL,
ServerName: string(serverName),
},
},
{
req: api.QueryProfileRequest{
UserID: fmt.Sprintf("@bob:%s", serverName),
},
wantRes: api.QueryProfileResponse{
UserExists: false,
},
userID: fmt.Sprintf("@bob:%s", serverName),
wantErr: api2.ErrProfileNotExists,
},
{
req: api.QueryProfileRequest{
UserID: "@alice:wrongdomain.com",
},
wantErr: fmt.Errorf("wrong domain"),
userID: "@alice:wrongdomain.com",
wantErr: api2.ErrProfileNotExists,
},
}
@ -147,14 +142,14 @@ func TestQueryProfile(t *testing.T) {
mode = "HTTP"
}
for _, tc := range testCases {
var gotRes api.QueryProfileResponse
gotErr := testAPI.QueryProfile(context.TODO(), &tc.req, &gotRes)
profile, gotErr := testAPI.QueryProfile(context.TODO(), tc.userID)
if tc.wantErr == nil && gotErr != nil || tc.wantErr != nil && gotErr == nil {
t.Errorf("QueryProfile %s error, got %s want %s", mode, gotErr, tc.wantErr)
continue
}
if !reflect.DeepEqual(tc.wantRes, gotRes) {
t.Errorf("QueryProfile %s response got %+v want %+v", mode, gotRes, tc.wantRes)
if !reflect.DeepEqual(tc.wantRes, profile) {
t.Errorf("QueryProfile %s response got %+v want %+v", mode, profile, tc.wantRes)
}
}
}