From 63a199cec35b307c29229083208961949d9deb3e Mon Sep 17 00:00:00 2001 From: Roman Isaev Date: Sun, 29 Dec 2024 23:53:37 +0000 Subject: [PATCH] mas: first successful attempt of login with via mas --- clientapi/admin_test.go | 42 +- clientapi/auth/auth.go | 47 -- clientapi/auth/authtypes/logintypes.go | 1 + clientapi/auth/default_user_verifier.go | 59 +++ clientapi/clientapi.go | 7 +- clientapi/routing/admin.go | 161 +++++++ clientapi/routing/key_crosssigning.go | 113 ++++- clientapi/routing/password.go | 1 + clientapi/routing/register.go | 6 +- clientapi/routing/routing.go | 260 +++++----- internal/httputil/httpapi.go | 26 +- mediaapi/mediaapi.go | 5 +- mediaapi/routing/routing.go | 10 +- setup/config/config_mscs.go | 9 +- setup/monolith.go | 30 +- setup/mscs/msc2836/msc2836.go | 4 +- setup/mscs/msc3861/msc3861.go | 17 + setup/mscs/msc3861/msc3861_user_verifier.go | 444 ++++++++++++++++++ setup/mscs/mscs.go | 6 +- syncapi/routing/routing.go | 25 +- syncapi/syncapi.go | 2 + userapi/api/api.go | 32 ++ userapi/internal/cross_signing.go | 16 +- userapi/internal/key_api.go | 13 + userapi/internal/user_api.go | 19 +- userapi/storage/interface.go | 3 +- .../postgres/cross_signing_keys_table.go | 60 ++- userapi/storage/shared/storage.go | 15 +- .../sqlite3/cross_signing_keys_table.go | 64 ++- userapi/storage/tables/interface.go | 7 +- userapi/types/storage.go | 7 +- 31 files changed, 1224 insertions(+), 287 deletions(-) create mode 100644 clientapi/auth/default_user_verifier.go create mode 100644 setup/mscs/msc3861/msc3861.go create mode 100644 setup/mscs/msc3861/msc3861_user_verifier.go diff --git a/clientapi/admin_test.go b/clientapi/admin_test.go index d3c5bcee..179e9140 100644 --- a/clientapi/admin_test.go +++ b/clientapi/admin_test.go @@ -27,6 +27,7 @@ import ( "github.com/tidwall/gjson" capi "github.com/element-hq/dendrite/clientapi/api" + "github.com/element-hq/dendrite/clientapi/auth" "github.com/element-hq/dendrite/test" "github.com/element-hq/dendrite/test/testrig" "github.com/element-hq/dendrite/userapi" @@ -48,7 +49,8 @@ func TestAdminCreateToken(t *testing.T) { rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics) rsAPI.SetFederationAPI(nil, nil) userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) - AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics) + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) accessTokens := map[*test.User]userDevice{ aliceAdmin: {}, bob: {}, @@ -199,7 +201,8 @@ func TestAdminListRegistrationTokens(t *testing.T) { rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics) rsAPI.SetFederationAPI(nil, nil) userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) - AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics) + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) accessTokens := map[*test.User]userDevice{ aliceAdmin: {}, bob: {}, @@ -317,7 +320,8 @@ func TestAdminGetRegistrationToken(t *testing.T) { rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics) rsAPI.SetFederationAPI(nil, nil) userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) - AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics) + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) accessTokens := map[*test.User]userDevice{ aliceAdmin: {}, bob: {}, @@ -418,7 +422,8 @@ func TestAdminDeleteRegistrationToken(t *testing.T) { rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics) rsAPI.SetFederationAPI(nil, nil) userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) - AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics) + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) accessTokens := map[*test.User]userDevice{ aliceAdmin: {}, bob: {}, @@ -512,7 +517,8 @@ func TestAdminUpdateRegistrationToken(t *testing.T) { rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics) rsAPI.SetFederationAPI(nil, nil) userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) - AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics) + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) accessTokens := map[*test.User]userDevice{ aliceAdmin: {}, bob: {}, @@ -697,7 +703,8 @@ func TestAdminResetPassword(t *testing.T) { // Needed for changing the password/login userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) // We mostly need the userAPI for this test, so nil for other APIs/caches etc. - AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, caching.DisableMetrics) + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) // Create the users in the userapi and login accessTokens := map[*test.User]userDevice{ @@ -801,8 +808,9 @@ func TestPurgeRoom(t *testing.T) { t.Fatalf("failed to send events: %v", err) } + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} // 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) + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) // Create the users in the userapi and login accessTokens := map[*test.User]userDevice{ @@ -872,8 +880,10 @@ func TestAdminEvacuateRoom(t *testing.T) { t.Fatalf("failed to send events: %v", err) } + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + // 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) + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) // Create the users in the userapi and login accessTokens := map[*test.User]userDevice{ @@ -976,8 +986,10 @@ func TestAdminEvacuateUser(t *testing.T) { t.Fatalf("failed to send events: %v", err) } + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + // 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) + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) // Create the users in the userapi and login accessTokens := map[*test.User]userDevice{ @@ -1059,8 +1071,10 @@ func TestAdminMarkAsStale(t *testing.T) { rsAPI.SetFederationAPI(nil, nil) userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff) + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + // 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) + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) // Create the users in the userapi and login accessTokens := map[*test.User]userDevice{ @@ -1147,8 +1161,10 @@ func TestAdminQueryEventReports(t *testing.T) { t.Fatalf("failed to send events: %v", err) } + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + // 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) + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) accessTokens := map[*test.User]userDevice{ alice: {}, @@ -1376,8 +1392,10 @@ func TestEventReportsGetDelete(t *testing.T) { t.Fatalf("failed to send events: %v", err) } + userVerifier := auth.DefaultUserVerifier{UserAPI: userAPI} + // 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) + AddPublicRoutes(processCtx, routers, cfg, &natsInstance, nil, rsAPI, nil, nil, nil, userAPI, nil, nil, &userVerifier, caching.DisableMetrics) accessTokens := map[*test.User]userDevice{ alice: {}, diff --git a/clientapi/auth/auth.go b/clientapi/auth/auth.go index c32ed0fa..4e3612ce 100644 --- a/clientapi/auth/auth.go +++ b/clientapi/auth/auth.go @@ -16,8 +16,6 @@ import ( "strings" "github.com/element-hq/dendrite/userapi/api" - "github.com/matrix-org/gomatrixserverlib/spec" - "github.com/matrix-org/util" ) // OWASP recommends at least 128 bits of entropy for tokens: https://www.owasp.org/index.php/Insufficient_Session-ID_Length @@ -37,51 +35,6 @@ type AccountDatabase interface { GetAccountByPassword(ctx context.Context, localpart, password string) (*api.Account, error) } -// VerifyUserFromRequest authenticates the HTTP request, -// on success returns Device of the requester. -// Finds local user or an application service user. -// Note: For an AS user, AS dummy device is returned. -// On failure returns an JSON error response which can be sent to the client. -func VerifyUserFromRequest( - req *http.Request, userAPI api.QueryAcccessTokenAPI, -) (*api.Device, *util.JSONResponse) { - // Try to find the Application Service user - token, err := ExtractAccessToken(req) - if err != nil { - return nil, &util.JSONResponse{ - Code: http.StatusUnauthorized, - JSON: spec.MissingToken(err.Error()), - } - } - var res api.QueryAccessTokenResponse - err = userAPI.QueryAccessToken(req.Context(), &api.QueryAccessTokenRequest{ - AccessToken: token, - AppServiceUserID: req.URL.Query().Get("user_id"), - }, &res) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed") - return nil, &util.JSONResponse{ - Code: http.StatusInternalServerError, - JSON: spec.InternalServerError{}, - } - } - if res.Err != "" { - if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison - return nil, &util.JSONResponse{ - Code: http.StatusForbidden, - JSON: spec.Forbidden(res.Err), - } - } - } - if res.Device == nil { - return nil, &util.JSONResponse{ - Code: http.StatusUnauthorized, - JSON: spec.UnknownToken("Unknown token"), - } - } - return res.Device, nil -} - // GenerateAccessToken creates a new access token. Returns an error if failed to generate // random bytes. func GenerateAccessToken() (string, error) { diff --git a/clientapi/auth/authtypes/logintypes.go b/clientapi/auth/authtypes/logintypes.go index f01e48f8..c6e67f31 100644 --- a/clientapi/auth/authtypes/logintypes.go +++ b/clientapi/auth/authtypes/logintypes.go @@ -11,4 +11,5 @@ const ( LoginTypeRecaptcha = "m.login.recaptcha" LoginTypeApplicationService = "m.login.application_service" LoginTypeToken = "m.login.token" + LoginTypeCrossSigningReset = "org.matrix.cross_signing_reset" ) diff --git a/clientapi/auth/default_user_verifier.go b/clientapi/auth/default_user_verifier.go new file mode 100644 index 00000000..f0a48f51 --- /dev/null +++ b/clientapi/auth/default_user_verifier.go @@ -0,0 +1,59 @@ +package auth + +import ( + "net/http" + "strings" + + "github.com/element-hq/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" +) + +// DefaultUserVerifier implements UserVerifier interface +type DefaultUserVerifier struct { + UserAPI api.QueryAcccessTokenAPI +} + +// VerifyUserFromRequest authenticates the HTTP request, +// on success returns Device of the requester. +// Finds local user or an application service user. +// Note: For an AS user, AS dummy device is returned. +// On failure returns an JSON error response which can be sent to the client. +func (d *DefaultUserVerifier) VerifyUserFromRequest(req *http.Request) (*api.Device, *util.JSONResponse) { + util.GetLogger(req.Context()).Debug("Default VerifyUserFromRequest") + // Try to find the Application Service user + token, err := ExtractAccessToken(req) + if err != nil { + return nil, &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: spec.MissingToken(err.Error()), + } + } + var res api.QueryAccessTokenResponse + err = d.UserAPI.QueryAccessToken(req.Context(), &api.QueryAccessTokenRequest{ + AccessToken: token, + AppServiceUserID: req.URL.Query().Get("user_id"), + }, &res) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed") + return nil, &util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{}, + } + } + if res.Err != "" { + if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison + return nil, &util.JSONResponse{ + Code: http.StatusForbidden, + JSON: spec.Forbidden(res.Err), + } + } + } + if res.Device == nil { + return nil, &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: spec.UnknownToken("Unknown token"), + } + } + return res.Device, nil +} diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go index dbf862ca..1c3bc471 100644 --- a/clientapi/clientapi.go +++ b/clientapi/clientapi.go @@ -36,7 +36,9 @@ func AddPublicRoutes( fsAPI federationAPI.ClientFederationAPI, userAPI userapi.ClientUserAPI, userDirectoryProvider userapi.QuerySearchProfilesAPI, - extRoomsProvider api.ExtraPublicRoomsProvider, enableMetrics bool, + extRoomsProvider api.ExtraPublicRoomsProvider, + userVerifier httputil.UserVerifier, + enableMetrics bool, ) { js, natsClient := natsInstance.Prepare(processContext, &cfg.Global.JetStream) @@ -55,6 +57,7 @@ func AddPublicRoutes( cfg, rsAPI, asAPI, userAPI, userDirectoryProvider, federation, syncProducer, transactionsCache, fsAPI, - extRoomsProvider, natsClient, enableMetrics, + extRoomsProvider, natsClient, + userVerifier, enableMetrics, ) } diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go index 40881166..0b07724a 100644 --- a/clientapi/routing/admin.go +++ b/clientapi/routing/admin.go @@ -21,6 +21,7 @@ import ( "golang.org/x/exp/constraints" clientapi "github.com/element-hq/dendrite/clientapi/api" + clienthttputil "github.com/element-hq/dendrite/clientapi/httputil" "github.com/element-hq/dendrite/internal/httputil" roomserverAPI "github.com/element-hq/dendrite/roomserver/api" "github.com/element-hq/dendrite/setup/config" @@ -517,6 +518,166 @@ func AdminCheckUsernameAvailable( } } +func AdminHandleUserDeviceByUserID( + req *http.Request, + userAPI userapi.ClientUserAPI, +) util.JSONResponse { + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + userID, ok := vars["userID"] + if !ok { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.MissingParam("Expecting user ID."), + } + } + + logger := util.GetLogger(req.Context()) + + switch req.Method { + case http.MethodPost: + local, domain, err := gomatrixserverlib.SplitID('@', userID) + if err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.InvalidParam(userID), + } + } + var payload struct { + DeviceID string `json:"device_id"` + } + if resErr := clienthttputil.UnmarshalJSONRequest(req, &payload); resErr != nil { + return *resErr + } + + var rs userapi.PerformDeviceCreationResponse + if err := userAPI.PerformDeviceCreation(req.Context(), &userapi.PerformDeviceCreationRequest{ + Localpart: local, + ServerName: domain, + DeviceID: &payload.DeviceID, + IPAddr: "", + UserAgent: req.UserAgent(), + NoDeviceListUpdate: false, + FromRegistration: false, + }, &rs); err != nil { + logger.WithError(err).Debug("PerformDeviceCreation failed") + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + + logger.WithError(err).Debug("PerformDeviceCreation succeeded") + return util.JSONResponse{ + Code: http.StatusCreated, + JSON: struct{}{}, + } + case http.MethodGet: + var res userapi.QueryDevicesResponse + if err := userAPI.QueryDevices(req.Context(), &userapi.QueryDevicesRequest{UserID: userID}, &res); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + + jsonDevices := make([]deviceJSON, 0, len(res.Devices)) + for i := range res.Devices { + d := &res.Devices[i] + jsonDevices = append(jsonDevices, deviceJSON{ + DeviceID: d.ID, + DisplayName: d.DisplayName, + LastSeenIP: d.LastSeenIP, + LastSeenTS: d.LastSeenTS, + }) + } + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: struct { + Devices []deviceJSON `json:"devices"` + Total int `json:"total"` + }{ + Devices: jsonDevices, + Total: len(res.Devices), + }, + } + default: + return util.JSONResponse{ + Code: http.StatusMethodNotAllowed, + JSON: struct{}{}, + } + } + +} + +type adminExternalID struct { + AuthProvider string `json:"auth_provider"` + ExternalID string `json:"external_id"` +} + +type adminCreateOrModifyAccountRequest struct { + DisplayName string `json:"display_name"` + AvatarURL string `json:"avatar_url"` + // TODO: the following fields are not used here, but they are used in Synapse. Probably we should reproduce the logic of the + // endpoint fully compatible. + // Password string `json:"password"` + // LogoutDevices bool `json:"logout_devices"` + // Threepids json.RawMessage `json:"threepids"` + // ExternalIDs []adminExternalID `json:"external_ids"` + // Admin bool `json:"admin"` + // Deactivated bool `json:"deactivated"` + // Locked bool `json:"locked"` +} + +func AdminCreateOrModifyAccount(req *http.Request, userAPI userapi.ClientUserAPI) util.JSONResponse { + logger := util.GetLogger(req.Context()) + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + userID, ok := vars["userID"] + if !ok { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.MissingParam("Expecting user ID."), + } + } + local, domain, err := gomatrixserverlib.SplitID('@', userID) + if err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.InvalidParam(userID), + } + } + var r adminCreateOrModifyAccountRequest + if resErr := clienthttputil.UnmarshalJSONRequest(req, &r); resErr != nil { + logger.Debugf("UnmarshalJSONRequest failed: %+v", *resErr) + return *resErr + } + logger.Debugf("adminCreateOrModifyAccountRequest is: %+v", r) + statusCode := http.StatusOK + { + var res userapi.PerformAccountCreationResponse + err = userAPI.PerformAccountCreation(req.Context(), &userapi.PerformAccountCreationRequest{ + AccountType: userapi.AccountTypeUser, + Localpart: local, + ServerName: domain, + OnConflict: api.ConflictUpdate, + AvatarURL: r.AvatarURL, + DisplayName: r.DisplayName, + }, &res) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Debugln("Failed creating account") + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + if res.AccountCreated { + statusCode = http.StatusCreated + } + } + + return util.JSONResponse{ + Code: statusCode, + JSON: nil, + } +} + // GetEventReports returns reported events for a given user/room. func GetEventReports( req *http.Request, diff --git a/clientapi/routing/key_crosssigning.go b/clientapi/routing/key_crosssigning.go index e6f093b5..7bcd7093 100644 --- a/clientapi/routing/key_crosssigning.go +++ b/clientapi/routing/key_crosssigning.go @@ -8,6 +8,8 @@ package routing import ( "net/http" + "strings" + "time" "github.com/element-hq/dendrite/clientapi/auth" "github.com/element-hq/dendrite/clientapi/auth/authtypes" @@ -39,28 +41,83 @@ func UploadCrossSigningDeviceKeys( if sessionID == "" { sessionID = util.RandomString(sessionIDLength) } - if uploadReq.Auth.Type != authtypes.LoginTypePassword { - return util.JSONResponse{ - Code: http.StatusUnauthorized, - JSON: newUserInteractiveResponse( - sessionID, - []authtypes.Flow{ - { - Stages: []authtypes.LoginType{authtypes.LoginTypePassword}, - }, - }, - nil, - ), + + isCrossSigningSetup := false + masterKeyUpdatableWithoutUIA := false + { + var keysResp api.QueryMasterKeysResponse + keyserverAPI.QueryMasterKeys(req.Context(), &api.QueryMasterKeysRequest{UserID: device.UserID}, &keysResp) + if err := keysResp.Error; err != nil { + return convertKeyError(err) + } + if k := keysResp.Key; k != nil { + isCrossSigningSetup = true + if k.UpdatableWithoutUIABeforeMs != nil { + masterKeyUpdatableWithoutUIA = time.Now().UnixMilli() < *k.UpdatableWithoutUIABeforeMs + } } } - typePassword := auth.LoginTypePassword{ - GetAccountByPassword: accountAPI.QueryAccountByPassword, - Config: cfg, + + if isCrossSigningSetup { + // With MSC3861, UIA is not possible. Instead, the auth service has to explicitly mark the master key as replaceable. + if cfg.MSCs.MSC3861Enabled() { + if !masterKeyUpdatableWithoutUIA { + url := "" + if m := cfg.MSCs.MSC3861; m.AccountManagementURL != "" { + url = strings.Join([]string{m.AccountManagementURL, "?action=", authtypes.LoginTypeCrossSigningReset}, "") + } else { + url = m.Issuer + } + return util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: newUserInteractiveResponse( + "dummy", + []authtypes.Flow{ + { + Stages: []authtypes.LoginType{authtypes.LoginTypeCrossSigningReset}, + }, + }, + map[string]interface{}{ + authtypes.LoginTypeCrossSigningReset: map[string]string{ + "url": url, + }, + }, + strings.Join([]string{ + "To reset your end-to-end encryption cross-signing, identity, you first need to approve it at", + url, + "and then try again.", + }, " "), + ), + } + } + // XXX: is it necessary? + sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeCrossSigningReset) + } else { + if uploadReq.Auth.Type != authtypes.LoginTypePassword { + return util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: newUserInteractiveResponse( + sessionID, + []authtypes.Flow{ + { + Stages: []authtypes.LoginType{authtypes.LoginTypePassword}, + }, + }, + nil, + "", + ), + } + } + typePassword := auth.LoginTypePassword{ + GetAccountByPassword: accountAPI.QueryAccountByPassword, + Config: cfg, + } + if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil { + return *authErr + } + sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword) + } } - if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil { - return *authErr - } - sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword) uploadReq.UserID = device.UserID keyserverAPI.PerformUploadDeviceKeys(req.Context(), &uploadReq.PerformUploadDeviceKeysRequest, uploadRes) @@ -108,7 +165,17 @@ func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.Clie keyserverAPI.PerformUploadDeviceSignatures(req.Context(), uploadReq, uploadRes) if err := uploadRes.Error; err != nil { - switch { + return convertKeyError(err) + } + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: struct{}{}, + } +} + +func convertKeyError(err *api.KeyError) util.JSONResponse { + switch { case err.IsInvalidSignature: return util.JSONResponse{ Code: http.StatusBadRequest, @@ -130,10 +197,4 @@ func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.Clie JSON: spec.Unknown(err.Error()), } } - } - - return util.JSONResponse{ - Code: http.StatusOK, - JSON: struct{}{}, - } } diff --git a/clientapi/routing/password.go b/clientapi/routing/password.go index 59d9594d..6258155d 100644 --- a/clientapi/routing/password.go +++ b/clientapi/routing/password.go @@ -67,6 +67,7 @@ func Password( }, }, nil, + "", ), } } diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go index 5544dccd..7bcda206 100644 --- a/clientapi/routing/register.go +++ b/clientapi/routing/register.go @@ -234,6 +234,7 @@ type userInteractiveResponse struct { Completed []authtypes.LoginType `json:"completed"` Params map[string]interface{} `json:"params"` Session string `json:"session"` + Msg string `json:"msg,omitempty"` } // newUserInteractiveResponse will return a struct to be sent back to the client @@ -242,9 +243,10 @@ func newUserInteractiveResponse( sessionID string, fs []authtypes.Flow, params map[string]interface{}, + msg string, ) userInteractiveResponse { return userInteractiveResponse{ - fs, sessions.getCompletedStages(sessionID), params, sessionID, + fs, sessions.getCompletedStages(sessionID), params, sessionID, msg, } } @@ -817,7 +819,7 @@ func checkAndCompleteFlow( return util.JSONResponse{ Code: http.StatusUnauthorized, JSON: newUserInteractiveResponse(sessionID, - cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params), + cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params, ""), } } diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 73cfcfc3..ed93d079 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -67,7 +67,9 @@ func Setup( transactionsCache *transactions.Cache, federationSender federationAPI.ClientFederationAPI, extRoomsProvider api.ExtraPublicRoomsProvider, - natsClient *nats.Conn, enableMetrics bool, + natsClient *nats.Conn, + userVerifier httputil.UserVerifier, + enableMetrics bool, ) { cfg := &dendriteCfg.ClientAPI mscCfg := &dendriteCfg.MSCs @@ -171,19 +173,19 @@ func Setup( ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) } dendriteAdminRouter.Handle("/admin/registrationTokens/new", - httputil.MakeAdminAPI("admin_registration_tokens_new", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_registration_tokens_new", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminCreateNewRegistrationToken(req, cfg, userAPI) }), ).Methods(http.MethodPost, http.MethodOptions) dendriteAdminRouter.Handle("/admin/registrationTokens", - httputil.MakeAdminAPI("admin_list_registration_tokens", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_list_registration_tokens", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminListRegistrationTokens(req, cfg, userAPI) }), ).Methods(http.MethodGet, http.MethodOptions) dendriteAdminRouter.Handle("/admin/registrationTokens/{token}", - httputil.MakeAdminAPI("admin_get_registration_token", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_get_registration_token", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { switch req.Method { case http.MethodGet: return AdminGetRegistrationToken(req, cfg, userAPI) @@ -202,43 +204,43 @@ func Setup( ).Methods(http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodOptions) dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}", - httputil.MakeAdminAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_evacuate_room", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminEvacuateRoom(req, rsAPI) }), ).Methods(http.MethodPost, http.MethodOptions) dendriteAdminRouter.Handle("/admin/evacuateUser/{userID}", - httputil.MakeAdminAPI("admin_evacuate_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_evacuate_user", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminEvacuateUser(req, rsAPI) }), ).Methods(http.MethodPost, http.MethodOptions) dendriteAdminRouter.Handle("/admin/purgeRoom/{roomID}", - httputil.MakeAdminAPI("admin_purge_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_purge_room", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminPurgeRoom(req, rsAPI) }), ).Methods(http.MethodPost, http.MethodOptions) dendriteAdminRouter.Handle("/admin/resetPassword/{userID}", - httputil.MakeAdminAPI("admin_reset_password", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_reset_password", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminResetPassword(req, cfg, device, userAPI) }), ).Methods(http.MethodPost, http.MethodOptions) dendriteAdminRouter.Handle("/admin/downloadState/{serverName}/{roomID}", - httputil.MakeAdminAPI("admin_download_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_download_state", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminDownloadState(req, device, rsAPI) }), ).Methods(http.MethodGet, http.MethodOptions) dendriteAdminRouter.Handle("/admin/fulltext/reindex", - httputil.MakeAdminAPI("admin_fultext_reindex", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_fultext_reindex", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminReindex(req, cfg, device, natsClient) }), ).Methods(http.MethodGet, http.MethodOptions) dendriteAdminRouter.Handle("/admin/refreshDevices/{userID}", - httputil.MakeAdminAPI("admin_refresh_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_refresh_devices", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return AdminMarkAsStale(req, cfg, userAPI) }), ).Methods(http.MethodPost, http.MethodOptions) @@ -252,7 +254,7 @@ func Setup( } synapseAdminRouter.Handle("/admin/v1/send_server_notice/{txnID}", - httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_server_notice", userVerifier, 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 @@ -273,7 +275,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) synapseAdminRouter.Handle("/admin/v1/send_server_notice", - httputil.MakeAuthAPI("send_server_notice", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_server_notice", userVerifier, 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 @@ -301,12 +303,12 @@ func Setup( unstableMux := publicAPIMux.PathPrefix("/unstable").Subrouter() v3mux.Handle("/createRoom", - httputil.MakeAuthAPI("createRoom", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("createRoom", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return CreateRoom(req, device, cfg, userAPI, rsAPI, asAPI) }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/join/{roomIDOrAlias}", - httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI(spec.Join, userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -340,11 +342,21 @@ func Setup( httputil.MakeServiceAdminAPI("admin_username_available", m.AdminToken, func(r *http.Request) util.JSONResponse { return AdminCheckUsernameAvailable(r, userAPI, cfg) })).Methods(http.MethodGet) + + synapseAdminRouter.Handle("/admin/v2/users/{userID}", + httputil.MakeServiceAdminAPI("admin_provision_user", m.AdminToken, func(r *http.Request) util.JSONResponse { + return AdminCreateOrModifyAccount(r, userAPI) + })).Methods(http.MethodPut) + + synapseAdminRouter.Handle("/admin/v2/users/{userID}/devices", + httputil.MakeServiceAdminAPI("admin_user_devices", m.AdminToken, func(r *http.Request) util.JSONResponse { + return AdminHandleUserDeviceByUserID(r, userAPI) + })).Methods(http.MethodPost, http.MethodGet) } if mscCfg.Enabled("msc2753") { v3mux.Handle("/peek/{roomIDOrAlias}", - httputil.MakeAuthAPI(spec.Peek, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI(spec.Peek, userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -359,12 +371,12 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) } v3mux.Handle("/joined_rooms", - httputil.MakeAuthAPI("joined_rooms", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("joined_rooms", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetJoinedRooms(req, device, rsAPI) }, httputil.WithAllowGuests()), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/join", - httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI(spec.Join, userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -386,7 +398,7 @@ func Setup( }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/leave", - httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("membership", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -400,7 +412,7 @@ func Setup( }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/unpeek", - httputil.MakeAuthAPI("unpeek", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("unpeek", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -411,7 +423,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/ban", - httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("membership", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -420,7 +432,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/invite", - httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("membership", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -432,7 +444,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/kick", - httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("membership", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -441,7 +453,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/unban", - httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("membership", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -450,7 +462,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/send/{eventType}", - httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_message", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -459,7 +471,7 @@ func Setup( }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/send/{eventType}/{txnID}", - httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_message", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -470,7 +482,7 @@ func Setup( }, httputil.WithAllowGuests()), ).Methods(http.MethodPut, http.MethodOptions) - v3mux.Handle("/rooms/{roomID}/state", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + v3mux.Handle("/rooms/{roomID}/state", httputil.MakeAuthAPI("room_state", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -478,7 +490,7 @@ func Setup( return OnIncomingStateRequest(req.Context(), device, rsAPI, vars["roomID"]) }, httputil.WithAllowGuests())).Methods(http.MethodGet, http.MethodOptions) - v3mux.Handle("/rooms/{roomID}/aliases", httputil.MakeAuthAPI("aliases", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + v3mux.Handle("/rooms/{roomID}/aliases", httputil.MakeAuthAPI("aliases", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -486,7 +498,7 @@ func Setup( return GetAliases(req, rsAPI, device, vars["roomID"]) })).Methods(http.MethodGet, http.MethodOptions) - v3mux.Handle("/rooms/{roomID}/state/{type:[^/]+/?}", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + v3mux.Handle("/rooms/{roomID}/state/{type:[^/]+/?}", httputil.MakeAuthAPI("room_state", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -497,7 +509,7 @@ func Setup( return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], eventType, "", eventFormat) }, httputil.WithAllowGuests())).Methods(http.MethodGet, http.MethodOptions) - v3mux.Handle("/rooms/{roomID}/state/{type}/{stateKey}", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + v3mux.Handle("/rooms/{roomID}/state/{type}/{stateKey}", httputil.MakeAuthAPI("room_state", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -507,7 +519,7 @@ func Setup( }, httputil.WithAllowGuests())).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/state/{eventType:[^/]+/?}", - httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_message", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -519,7 +531,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/state/{eventType}/{stateKey}", - httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_message", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -533,7 +545,7 @@ func Setup( // TODO: clear based on some criteria roomHierarchyPaginationCache := NewRoomHierarchyPaginationCache() v1mux.Handle("/rooms/{roomID}/hierarchy", - httputil.MakeAuthAPI("spaces", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("spaces", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -567,7 +579,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/directory/room/{roomAlias}", - httputil.MakeAuthAPI("directory_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("directory_room", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -577,7 +589,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/directory/room/{roomAlias}", - httputil.MakeAuthAPI("directory_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("directory_room", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -596,7 +608,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/directory/list/room/{roomID}", - httputil.MakeAuthAPI("directory_list", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("directory_list", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -605,7 +617,7 @@ func Setup( }), ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/directory/list/appservice/{networkID}/{roomID}", - httputil.MakeAuthAPI("directory_list", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("directory_list", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -616,7 +628,7 @@ func Setup( // Undocumented endpoint v3mux.Handle("/directory/list/appservice/{networkID}/{roomID}", - httputil.MakeAuthAPI("directory_list", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("directory_list", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -632,19 +644,19 @@ func Setup( ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) v3mux.Handle("/logout", - httputil.MakeAuthAPI("logout", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("logout", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return Logout(req, userAPI, device) }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/logout/all", - httputil.MakeAuthAPI("logout", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("logout", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return LogoutAll(req, userAPI, device) }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/typing/{userID}", - httputil.MakeAuthAPI("rooms_typing", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_typing", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -656,7 +668,7 @@ func Setup( }), ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/redact/{eventID}", - httputil.MakeAuthAPI("rooms_redact", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_redact", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -665,7 +677,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/redact/{eventID}/{txnId}", - httputil.MakeAuthAPI("rooms_redact", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_redact", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -676,7 +688,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/sendToDevice/{eventType}/{txnID}", - httputil.MakeAuthAPI("send_to_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_to_device", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -690,7 +702,7 @@ func Setup( // rather than r0. It's an exact duplicate of the above handler. // TODO: Remove this if/when sytest is fixed! unstableMux.Handle("/sendToDevice/{eventType}/{txnID}", - httputil.MakeAuthAPI("send_to_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("send_to_device", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -701,7 +713,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/account/whoami", - httputil.MakeAuthAPI("whoami", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("whoami", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -710,7 +722,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/account/password", - httputil.MakeAuthAPI("password", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("password", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -719,7 +731,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/account/deactivate", - httputil.MakeAuthAPI("deactivate", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("deactivate", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -739,7 +751,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) v3mux.Handle("/auth/{authType}/fallback/web", - httputil.MakeHTTPAPI("auth_fallback", userAPI, enableMetrics, func(w http.ResponseWriter, req *http.Request) { + httputil.MakeHTTPAPI("auth_fallback", userVerifier, enableMetrics, func(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) AuthFallback(w, req, vars["authType"], cfg) }), @@ -748,7 +760,7 @@ func Setup( // Push rules v3mux.Handle("/pushrules", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.InvalidParam("missing trailing slash"), @@ -757,13 +769,13 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetAllPushRules(req.Context(), device, userAPI) }), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.InvalidParam("scope, kind and rule ID must be specified"), @@ -772,7 +784,7 @@ func Setup( ).Methods(http.MethodPut) v3mux.Handle("/pushrules/{scope}/", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -782,7 +794,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/{scope}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.InvalidParam("missing trailing slash after scope"), @@ -791,7 +803,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/{scope:[^/]+/?}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.InvalidParam("kind and rule ID must be specified"), @@ -800,7 +812,7 @@ func Setup( ).Methods(http.MethodPut) v3mux.Handle("/pushrules/{scope}/{kind}/", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -810,7 +822,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/{scope}/{kind}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.InvalidParam("missing trailing slash after kind"), @@ -819,7 +831,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/{scope}/{kind:[^/]+/?}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: spec.InvalidParam("rule ID must be specified"), @@ -828,7 +840,7 @@ func Setup( ).Methods(http.MethodPut) v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -838,7 +850,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -852,7 +864,7 @@ func Setup( ).Methods(http.MethodPut) v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -862,7 +874,7 @@ func Setup( ).Methods(http.MethodDelete) v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}/{attr}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -872,7 +884,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushrules/{scope}/{kind}/{ruleID}/{attr}", - httputil.MakeAuthAPI("push_rules", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("push_rules", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -904,7 +916,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/profile/{userID}/avatar_url", - httputil.MakeAuthAPI("profile_avatar_url", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("profile_avatar_url", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -929,7 +941,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/profile/{userID}/displayname", - httputil.MakeAuthAPI("profile_displayname", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("profile_displayname", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -946,19 +958,19 @@ func Setup( threePIDClient := base.CreateClient(dendriteCfg, nil) // TODO: Move this somewhere else, e.g. pass in as parameter v3mux.Handle("/account/3pid", - httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("account_3pid", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetAssociated3PIDs(req, userAPI, device) }), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/account/3pid", - httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("account_3pid", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return CheckAndSave3PIDAssociation(req, userAPI, device, cfg, threePIDClient) }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/account/3pid/delete", - httputil.MakeAuthAPI("account_3pid", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("account_3pid", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return Forget3PID(req, userAPI) }), ).Methods(http.MethodPost, http.MethodOptions) @@ -970,7 +982,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/voip/turnServer", - httputil.MakeAuthAPI("turn_server", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("turn_server", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -979,13 +991,13 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/thirdparty/protocols", - httputil.MakeAuthAPI("thirdparty_protocols", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("thirdparty_protocols", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return Protocols(req, asAPI, device, "") }, httputil.WithAllowGuests()), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/thirdparty/protocol/{protocolID}", - httputil.MakeAuthAPI("thirdparty_protocols", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("thirdparty_protocols", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -995,7 +1007,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/thirdparty/user/{protocolID}", - httputil.MakeAuthAPI("thirdparty_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("thirdparty_user", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1005,13 +1017,13 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/thirdparty/user", - httputil.MakeAuthAPI("thirdparty_user", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("thirdparty_user", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return User(req, asAPI, device, "", req.URL.Query()) }, httputil.WithAllowGuests()), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/thirdparty/location/{protocolID}", - httputil.MakeAuthAPI("thirdparty_location", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("thirdparty_location", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1021,7 +1033,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/thirdparty/location", - httputil.MakeAuthAPI("thirdparty_location", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("thirdparty_location", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return Location(req, asAPI, device, "", req.URL.Query()) }, httputil.WithAllowGuests()), ).Methods(http.MethodGet, http.MethodOptions) @@ -1037,7 +1049,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/user/{userID}/account_data/{type}", - httputil.MakeAuthAPI("user_account_data", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("user_account_data", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1047,7 +1059,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/user/{userID}/rooms/{roomID}/account_data/{type}", - httputil.MakeAuthAPI("user_account_data", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("user_account_data", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1057,7 +1069,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/user/{userID}/account_data/{type}", - httputil.MakeAuthAPI("user_account_data", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("user_account_data", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1067,7 +1079,7 @@ func Setup( ).Methods(http.MethodGet) v3mux.Handle("/user/{userID}/rooms/{roomID}/account_data/{type}", - httputil.MakeAuthAPI("user_account_data", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("user_account_data", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1077,7 +1089,7 @@ func Setup( ).Methods(http.MethodGet) v3mux.Handle("/admin/whois/{userID}", - httputil.MakeAuthAPI("admin_whois", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("admin_whois", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1087,7 +1099,7 @@ func Setup( ).Methods(http.MethodGet) v3mux.Handle("/user/{userID}/openid/request_token", - httputil.MakeAuthAPI("openid_request_token", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("openid_request_token", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -1100,7 +1112,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/user_directory/search", - httputil.MakeAuthAPI("userdirectory_search", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("userdirectory_search", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -1126,7 +1138,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/read_markers", - httputil.MakeAuthAPI("rooms_read_markers", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_read_markers", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -1139,7 +1151,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/forget", - httputil.MakeAuthAPI("rooms_forget", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_forget", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -1152,7 +1164,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/upgrade", - httputil.MakeAuthAPI("rooms_upgrade", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_upgrade", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1162,13 +1174,13 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/devices", - httputil.MakeAuthAPI("get_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("get_devices", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetDevicesByLocalpart(req, userAPI, device) }, httputil.WithAllowGuests()), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/devices/{deviceID}", - httputil.MakeAuthAPI("get_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("get_device", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1178,7 +1190,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/devices/{deviceID}", - httputil.MakeAuthAPI("device_data", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("device_data", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1188,7 +1200,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/devices/{deviceID}", - httputil.MakeAuthAPI("delete_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("delete_device", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1198,25 +1210,25 @@ func Setup( ).Methods(http.MethodDelete, http.MethodOptions) v3mux.Handle("/delete_devices", - httputil.MakeAuthAPI("delete_devices", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("delete_devices", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return DeleteDevices(req, userInteractiveAuth, userAPI, device) }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/notifications", - httputil.MakeAuthAPI("get_notifications", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("get_notifications", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetNotifications(req, device, userAPI) }), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushers", - httputil.MakeAuthAPI("get_pushers", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("get_pushers", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetPushers(req, device, userAPI) }), ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/pushers/set", - httputil.MakeAuthAPI("set_pushers", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("set_pushers", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -1226,7 +1238,7 @@ func Setup( // Stub implementations for sytest v3mux.Handle("/events", - httputil.MakeAuthAPI("events", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("events", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{Code: http.StatusOK, JSON: map[string]interface{}{ "chunk": []interface{}{}, "start": "", @@ -1236,7 +1248,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/initialSync", - httputil.MakeAuthAPI("initial_sync", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("initial_sync", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return util.JSONResponse{Code: http.StatusOK, JSON: map[string]interface{}{ "end": "", }} @@ -1244,7 +1256,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/user/{userId}/rooms/{roomId}/tags", - httputil.MakeAuthAPI("get_tags", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("get_tags", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1254,7 +1266,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/user/{userId}/rooms/{roomId}/tags/{tag}", - httputil.MakeAuthAPI("put_tag", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("put_tag", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1264,7 +1276,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/user/{userId}/rooms/{roomId}/tags/{tag}", - httputil.MakeAuthAPI("delete_tag", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("delete_tag", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1274,7 +1286,7 @@ func Setup( ).Methods(http.MethodDelete, http.MethodOptions) v3mux.Handle("/capabilities", - httputil.MakeAuthAPI("capabilities", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("capabilities", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -1284,7 +1296,7 @@ func Setup( // Key Backup Versions (Metadata) - getBackupKeysVersion := httputil.MakeAuthAPI("get_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + getBackupKeysVersion := httputil.MakeAuthAPI("get_backup_keys_version", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1292,11 +1304,11 @@ func Setup( return KeyBackupVersion(req, userAPI, device, vars["version"]) }) - getLatestBackupKeysVersion := httputil.MakeAuthAPI("get_latest_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + getLatestBackupKeysVersion := httputil.MakeAuthAPI("get_latest_backup_keys_version", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return KeyBackupVersion(req, userAPI, device, "") }) - putBackupKeysVersion := httputil.MakeAuthAPI("put_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + putBackupKeysVersion := httputil.MakeAuthAPI("put_backup_keys_version", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1304,7 +1316,7 @@ func Setup( return ModifyKeyBackupVersionAuthData(req, userAPI, device, vars["version"]) }) - deleteBackupKeysVersion := httputil.MakeAuthAPI("delete_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + deleteBackupKeysVersion := httputil.MakeAuthAPI("delete_backup_keys_version", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1312,7 +1324,7 @@ func Setup( return DeleteKeyBackupVersion(req, userAPI, device, vars["version"]) }) - postNewBackupKeysVersion := httputil.MakeAuthAPI("post_new_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + postNewBackupKeysVersion := httputil.MakeAuthAPI("post_new_backup_keys_version", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return CreateKeyBackupVersion(req, userAPI, device) }) @@ -1331,7 +1343,7 @@ func Setup( // Inserting E2E Backup Keys // Bulk room and session - putBackupKeys := httputil.MakeAuthAPI("put_backup_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + putBackupKeys := httputil.MakeAuthAPI("put_backup_keys", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { version := req.URL.Query().Get("version") if version == "" { return util.JSONResponse{ @@ -1348,7 +1360,7 @@ func Setup( }) // Single room bulk session - putBackupKeysRoom := httputil.MakeAuthAPI("put_backup_keys_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + putBackupKeysRoom := httputil.MakeAuthAPI("put_backup_keys_room", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1380,7 +1392,7 @@ func Setup( }) // Single room, single session - putBackupKeysRoomSession := httputil.MakeAuthAPI("put_backup_keys_room_session", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + putBackupKeysRoomSession := httputil.MakeAuthAPI("put_backup_keys_room_session", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1422,11 +1434,11 @@ func Setup( // Querying E2E Backup Keys - getBackupKeys := httputil.MakeAuthAPI("get_backup_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + getBackupKeys := httputil.MakeAuthAPI("get_backup_keys", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return GetBackupKeys(req, userAPI, device, req.URL.Query().Get("version"), "", "") }) - getBackupKeysRoom := httputil.MakeAuthAPI("get_backup_keys_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + getBackupKeysRoom := httputil.MakeAuthAPI("get_backup_keys_room", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1434,7 +1446,7 @@ func Setup( return GetBackupKeys(req, userAPI, device, req.URL.Query().Get("version"), vars["roomID"], "") }) - getBackupKeysRoomSession := httputil.MakeAuthAPI("get_backup_keys_room_session", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + getBackupKeysRoomSession := httputil.MakeAuthAPI("get_backup_keys_room_session", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1454,11 +1466,11 @@ func Setup( // Cross-signing device keys - postDeviceSigningKeys := httputil.MakeAuthAPI("post_device_signing_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + postDeviceSigningKeys := httputil.MakeAuthAPI("post_device_signing_keys", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return UploadCrossSigningDeviceKeys(req, userInteractiveAuth, userAPI, device, userAPI, cfg) }) - postDeviceSigningSignatures := httputil.MakeAuthAPI("post_device_signing_signatures", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + postDeviceSigningSignatures := httputil.MakeAuthAPI("post_device_signing_signatures", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return UploadCrossSigningDeviceSignatures(req, userAPI, device) }, httputil.WithAllowGuests()) @@ -1470,27 +1482,27 @@ func Setup( // Supplying a device ID is deprecated. v3mux.Handle("/keys/upload/{deviceID}", - httputil.MakeAuthAPI("keys_upload", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("keys_upload", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return UploadKeys(req, userAPI, device) }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/keys/upload", - httputil.MakeAuthAPI("keys_upload", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("keys_upload", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return UploadKeys(req, userAPI, device) }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/keys/query", - httputil.MakeAuthAPI("keys_query", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("keys_query", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return QueryKeys(req, userAPI, device) }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/keys/claim", - httputil.MakeAuthAPI("keys_claim", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("keys_claim", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return ClaimKeys(req, userAPI) }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomId}/receipt/{receiptType}/{eventId}", - httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI(spec.Join, userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -1503,7 +1515,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/presence/{userId}/status", - httputil.MakeAuthAPI("set_presence", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("set_presence", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1512,7 +1524,7 @@ func Setup( }), ).Methods(http.MethodPut, http.MethodOptions) v3mux.Handle("/presence/{userId}/status", - httputil.MakeAuthAPI("get_presence", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("get_presence", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1522,7 +1534,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/joined_members", - httputil.MakeAuthAPI("rooms_members", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_members", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1532,7 +1544,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/report/{eventID}", - httputil.MakeAuthAPI("report_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("report_event", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1542,7 +1554,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) synapseAdminRouter.Handle("/admin/v1/event_reports", - httputil.MakeAdminAPI("admin_report_events", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_report_events", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { from := parseUint64OrDefault(req.URL.Query().Get("from"), 0) limit := parseUint64OrDefault(req.URL.Query().Get("limit"), 100) dir := req.URL.Query().Get("dir") @@ -1556,7 +1568,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) synapseAdminRouter.Handle("/admin/v1/event_reports/{reportID}", - httputil.MakeAdminAPI("admin_report_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_report_event", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -1566,7 +1578,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) synapseAdminRouter.Handle("/admin/v1/event_reports/{reportID}", - httputil.MakeAdminAPI("admin_report_event_delete", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAdminAPI("admin_report_event_delete", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) diff --git a/internal/httputil/httpapi.go b/internal/httputil/httpapi.go index 5a332d6f..f04c2bd4 100644 --- a/internal/httputil/httpapi.go +++ b/internal/httputil/httpapi.go @@ -58,17 +58,23 @@ func WithAuth() AuthAPIOption { } } +type UserVerifier interface { + // VerifyUserFromRequest authenticates the HTTP request, + // on success returns Device of the requester. + VerifyUserFromRequest(req *http.Request) (*userapi.Device, *util.JSONResponse) +} + // MakeAuthAPI turns a util.JSONRequestHandler function into an http.Handler which authenticates the request. func MakeAuthAPI( - metricsName string, userAPI userapi.QueryAcccessTokenAPI, + metricsName string, userVerifier UserVerifier, f func(*http.Request, *userapi.Device) util.JSONResponse, checks ...AuthAPIOption, ) http.Handler { h := func(req *http.Request) util.JSONResponse { logger := util.GetLogger(req.Context()) - device, err := auth.VerifyUserFromRequest(req, userAPI) + device, err := userVerifier.VerifyUserFromRequest(req) if err != nil { - logger.Debugf("VerifyUserFromRequest %s -> HTTP %d", req.RemoteAddr, err.Code) + logger.Debugf("VerifyUserFromRequest %s -> HTTP %d: JSON %+v", req.RemoteAddr, err.Code, err.JSON) return *err } // add the user ID to the logger @@ -122,11 +128,11 @@ func MakeAuthAPI( // MakeAdminAPI is a wrapper around MakeAuthAPI which enforces that the request can only be // completed by a user that is a server administrator. func MakeAdminAPI( - metricsName string, userAPI userapi.QueryAcccessTokenAPI, + metricsName string, userVerifier UserVerifier, f func(*http.Request, *userapi.Device) util.JSONResponse, ) http.Handler { - return MakeAuthAPI(metricsName, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { - if device.AccountType != userapi.AccountTypeAdmin { + return MakeAuthAPI(metricsName, userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { + if device == nil || device.AccountType != userapi.AccountTypeAdmin { return util.JSONResponse{ Code: http.StatusForbidden, JSON: spec.Forbidden("This API can only be used by admin users."), @@ -136,8 +142,8 @@ func MakeAdminAPI( }) } -// MakeServiceAdminAPI is a wrapper around MakeAuthAPI which enforces that the request can only be -// completed by a trusted service e.g. Matrix Auth Service. +// MakeServiceAdminAPI is a wrapper around MakeExternalAPI which enforces that the request can only be +// completed by a trusted service e.g. Matrix Auth Service (MAS). func MakeServiceAdminAPI( metricsName, serviceToken string, f func(*http.Request) util.JSONResponse, @@ -232,7 +238,7 @@ func MakeExternalAPI(metricsName string, f func(*http.Request) util.JSONResponse // MakeHTTPAPI adds Span metrics to the HTML Handler function // This is used to serve HTML alongside JSON error messages -func MakeHTTPAPI(metricsName string, userAPI userapi.QueryAcccessTokenAPI, enableMetrics bool, f func(http.ResponseWriter, *http.Request), checks ...AuthAPIOption) http.Handler { +func MakeHTTPAPI(metricsName string, userVerifier UserVerifier, enableMetrics bool, f func(http.ResponseWriter, *http.Request), checks ...AuthAPIOption) http.Handler { withSpan := func(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodOptions { util.SetCORSHeaders(w) @@ -252,7 +258,7 @@ func MakeHTTPAPI(metricsName string, userAPI userapi.QueryAcccessTokenAPI, enabl if opts.WithAuth { logger := util.GetLogger(req.Context()) - _, jsonErr := auth.VerifyUserFromRequest(req, userAPI) + _, jsonErr := userVerifier.VerifyUserFromRequest(req) if jsonErr != nil { w.WriteHeader(jsonErr.Code) if err := json.NewEncoder(w).Encode(jsonErr.JSON); err != nil { diff --git a/mediaapi/mediaapi.go b/mediaapi/mediaapi.go index ac20c886..9793d840 100644 --- a/mediaapi/mediaapi.go +++ b/mediaapi/mediaapi.go @@ -12,7 +12,6 @@ import ( "github.com/element-hq/dendrite/mediaapi/routing" "github.com/element-hq/dendrite/mediaapi/storage" "github.com/element-hq/dendrite/setup/config" - userapi "github.com/element-hq/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/fclient" "github.com/sirupsen/logrus" @@ -23,10 +22,10 @@ func AddPublicRoutes( routers httputil.Routers, cm *sqlutil.Connections, cfg *config.Dendrite, - userAPI userapi.MediaUserAPI, client *fclient.Client, fedClient fclient.FederationClient, keyRing gomatrixserverlib.JSONVerifier, + userVerifier httputil.UserVerifier, ) { mediaDB, err := storage.NewMediaAPIDatasource(cm, &cfg.MediaAPI.Database) if err != nil { @@ -34,6 +33,6 @@ func AddPublicRoutes( } routing.Setup( - routers, cfg, mediaDB, userAPI, client, fedClient, keyRing, + routers, cfg, mediaDB, client, fedClient, keyRing, userVerifier, ) } diff --git a/mediaapi/routing/routing.go b/mediaapi/routing/routing.go index 45da8eba..3d198f0d 100644 --- a/mediaapi/routing/routing.go +++ b/mediaapi/routing/routing.go @@ -42,10 +42,10 @@ func Setup( routers httputil.Routers, cfg *config.Dendrite, db storage.Database, - userAPI userapi.MediaUserAPI, client *fclient.Client, federationClient fclient.FederationClient, keyRing gomatrixserverlib.JSONVerifier, + userVerifier httputil.UserVerifier, ) { rateLimits := httputil.NewRateLimits(&cfg.ClientAPI.RateLimiting) @@ -58,7 +58,7 @@ func Setup( } uploadHandler := httputil.MakeAuthAPI( - "upload", userAPI, + "upload", userVerifier, func(req *http.Request, dev *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, dev); r != nil { return *r @@ -67,7 +67,7 @@ func Setup( }, ) - configHandler := httputil.MakeAuthAPI("config", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + configHandler := httputil.MakeAuthAPI("config", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { return *r } @@ -97,13 +97,13 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) // v1 client endpoints requiring auth - downloadHandlerAuthed := httputil.MakeHTTPAPI("download", userAPI, cfg.Global.Metrics.Enabled, makeDownloadAPI("download_authed_client", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, false), httputil.WithAuth()) + downloadHandlerAuthed := httputil.MakeHTTPAPI("download", userVerifier, cfg.Global.Metrics.Enabled, makeDownloadAPI("download_authed_client", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, false), httputil.WithAuth()) v1mux.Handle("/config", configHandler).Methods(http.MethodGet, http.MethodOptions) v1mux.Handle("/download/{serverName}/{mediaId}", downloadHandlerAuthed).Methods(http.MethodGet, http.MethodOptions) v1mux.Handle("/download/{serverName}/{mediaId}/{downloadName}", downloadHandlerAuthed).Methods(http.MethodGet, http.MethodOptions) v1mux.Handle("/thumbnail/{serverName}/{mediaId}", - httputil.MakeHTTPAPI("thumbnail", userAPI, cfg.Global.Metrics.Enabled, makeDownloadAPI("thumbnail_authed_client", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, false), httputil.WithAuth()), + httputil.MakeHTTPAPI("thumbnail", userVerifier, cfg.Global.Metrics.Enabled, makeDownloadAPI("thumbnail_authed_client", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, false), httputil.WithAuth()), ).Methods(http.MethodGet, http.MethodOptions) // same, but for federation diff --git a/setup/config/config_mscs.go b/setup/config/config_mscs.go index d6a51b65..1523a9ce 100644 --- a/setup/config/config_mscs.go +++ b/setup/config/config_mscs.go @@ -1,15 +1,18 @@ package config +import "slices" + type MSCs struct { Matrix *Global `yaml:"-"` // The MSCs to enable. Supported MSCs include: + // 'msc3861': Delegate auth to an OIDC provider. This line MUST always go first if the msc is used https://github.com/matrix-org/matrix-spec-proposals/pull/3861 // 'msc2444': Peeking over federation - https://github.com/matrix-org/matrix-doc/pull/2444 // 'msc2753': Peeking via /sync - https://github.com/matrix-org/matrix-doc/pull/2753 // 'msc2836': Threading - https://github.com/matrix-org/matrix-doc/pull/2836 - // 'msc3861': Delegate auth to an OIDC provider https://github.com/matrix-org/matrix-spec-proposals/pull/3861 MSCs []string `yaml:"mscs"` + // MSC3861 contains config related to the experimental feature MSC3861. It takes effect only if 'msc3861' is included in 'MSCs' array MSC3861 *MSC3861 `yaml:"msc3861,omitempty"` Database DatabaseOptions `yaml:"database,omitempty"` @@ -42,6 +45,10 @@ func (c *MSCs) Verify(configErrs *ConfigErrors) { } } +func (c *MSCs) MSC3861Enabled() bool { + return slices.Contains(c.MSCs, "msc3861") && c.MSC3861 != nil && c.MSC3861.Enabled +} + type MSC3861 struct { Enabled bool `yaml:"enabled"` Issuer string `yaml:"issuer"` diff --git a/setup/monolith.go b/setup/monolith.go index 36d6794d..8d8fadc9 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -7,9 +7,12 @@ package setup import ( + "net/http" + appserviceAPI "github.com/element-hq/dendrite/appservice/api" "github.com/element-hq/dendrite/clientapi" "github.com/element-hq/dendrite/clientapi/api" + "github.com/element-hq/dendrite/clientapi/auth" "github.com/element-hq/dendrite/federationapi" federationAPI "github.com/element-hq/dendrite/federationapi/api" "github.com/element-hq/dendrite/internal/caching" @@ -27,6 +30,7 @@ import ( userapi "github.com/element-hq/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/fclient" + "github.com/matrix-org/util" ) // Monolith represents an instantiation of all dependencies required to build @@ -46,6 +50,8 @@ type Monolith struct { // Optional ExtPublicRoomsProvider api.ExtraPublicRoomsProvider ExtUserDirectoryProvider userapi.QuerySearchProfilesAPI + + UserVerifierProvider *UserVerifierProvider } // AddAllPublicRoutes attaches all public paths to the given router @@ -58,6 +64,10 @@ func (m *Monolith) AddAllPublicRoutes( caches *caching.Caches, enableMetrics bool, ) { + if m.UserVerifierProvider == nil { + m.UserVerifierProvider = NewUserVerifierProvider(&auth.DefaultUserVerifier{UserAPI: m.UserAPI}) + } + userDirectoryProvider := m.ExtUserDirectoryProvider if userDirectoryProvider == nil { userDirectoryProvider = m.UserAPI @@ -65,15 +75,29 @@ func (m *Monolith) AddAllPublicRoutes( clientapi.AddPublicRoutes( processCtx, routers, cfg, natsInstance, m.FedClient, m.RoomserverAPI, m.AppserviceAPI, transactions.New(), m.FederationAPI, m.UserAPI, userDirectoryProvider, - m.ExtPublicRoomsProvider, enableMetrics, + m.ExtPublicRoomsProvider, m.UserVerifierProvider, enableMetrics, ) federationapi.AddPublicRoutes( processCtx, routers, cfg, natsInstance, m.UserAPI, m.FedClient, m.KeyRing, m.RoomserverAPI, m.FederationAPI, enableMetrics, ) - mediaapi.AddPublicRoutes(routers, cm, cfg, m.UserAPI, m.Client, m.FedClient, m.KeyRing) - syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, m.UserAPI, m.RoomserverAPI, caches, enableMetrics) + mediaapi.AddPublicRoutes(routers, cm, cfg, m.Client, m.FedClient, m.KeyRing, m.UserVerifierProvider) + syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, m.UserAPI, m.RoomserverAPI, caches, m.UserVerifierProvider, enableMetrics) if m.RelayAPI != nil { relayapi.AddPublicRoutes(routers, cfg, m.KeyRing, m.RelayAPI) } } + +type UserVerifierProvider struct { + UserVerifier httputil.UserVerifier +} + +func (u *UserVerifierProvider) VerifyUserFromRequest(req *http.Request) (*userapi.Device, *util.JSONResponse) { + return u.UserVerifier.VerifyUserFromRequest(req) +} + +func NewUserVerifierProvider(userVerifier httputil.UserVerifier) *UserVerifierProvider { + return &UserVerifierProvider{ + UserVerifier: userVerifier, + } +} diff --git a/setup/mscs/msc2836/msc2836.go b/setup/mscs/msc2836/msc2836.go index 4322e8a2..847e836a 100644 --- a/setup/mscs/msc2836/msc2836.go +++ b/setup/mscs/msc2836/msc2836.go @@ -98,7 +98,7 @@ func toClientResponse(ctx context.Context, res *MSC2836EventRelationshipsRespons // Enable this MSC func Enable( cfg *config.Dendrite, cm *sqlutil.Connections, routers httputil.Routers, rsAPI roomserver.RoomserverInternalAPI, fsAPI fs.FederationInternalAPI, - userAPI userapi.UserInternalAPI, keyRing gomatrixserverlib.JSONVerifier, + userVerifier httputil.UserVerifier, keyRing gomatrixserverlib.JSONVerifier, ) error { db, err := NewDatabase(cm, &cfg.MSCs.Database) if err != nil { @@ -124,7 +124,7 @@ func Enable( }) routers.Client.Handle("/unstable/event_relationships", - httputil.MakeAuthAPI("eventRelationships", userAPI, eventRelationshipHandler(db, rsAPI, fsAPI)), + httputil.MakeAuthAPI("eventRelationships", userVerifier, eventRelationshipHandler(db, rsAPI, fsAPI)), ).Methods(http.MethodPost, http.MethodOptions) routers.Federation.Handle("/unstable/event_relationships", httputil.MakeExternalAPI( diff --git a/setup/mscs/msc3861/msc3861.go b/setup/mscs/msc3861/msc3861.go new file mode 100644 index 00000000..9b38af31 --- /dev/null +++ b/setup/mscs/msc3861/msc3861.go @@ -0,0 +1,17 @@ +package msc3861 + +import ( + "github.com/element-hq/dendrite/setup" +) + +func Enable(m *setup.Monolith) error { + userVerifier, err := newMSC3861UserVerifier( + m.UserAPI, m.Config.Global.ServerName, + m.Config.MSCs.MSC3861, !m.Config.ClientAPI.GuestsDisabled, + ) + if err != nil { + return err + } + m.UserVerifierProvider.UserVerifier = userVerifier + return nil +} diff --git a/setup/mscs/msc3861/msc3861_user_verifier.go b/setup/mscs/msc3861/msc3861_user_verifier.go new file mode 100644 index 00000000..fcf5bb39 --- /dev/null +++ b/setup/mscs/msc3861/msc3861_user_verifier.go @@ -0,0 +1,444 @@ +package msc3861 + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "slices" + "strings" + + "github.com/element-hq/dendrite/clientapi/auth" + "github.com/element-hq/dendrite/setup/config" + "github.com/element-hq/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" +) + +const externalAuthProvider string = "oauth-delegated" + +// Scopes as defined by MSC2967 +// https://github.com/matrix-org/matrix-spec-proposals/pull/2967 +const ( + scopeMatrixAPI string = "urn:matrix:org.matrix.msc2967.client:api:*" + scopeMatrixGuest string = "urn:matrix:org.matrix.msc2967.client:api:guest" + scopeMatrixDevicePrefix string = "urn:matrix:org.matrix.msc2967.client:device:" +) + +type errCode string + +const ( + codeIntrospectionNot2xx errCode = "introspectionIsNot2xx" + codeInvalidClientToken errCode = "invalidClientToken" + codeAuthError errCode = "authError" + codeMxidError errCode = "mxidError" + codeOpenidConfigEndpointNon2xx errCode = "openidConfigEndpointNon2xx" + codeOpenidConfigDecodingFailed errCode = "openidConfigDecodingFailed" +) + +// MSC3861UserVerifier implements UserVerifier interface +type MSC3861UserVerifier struct { + userAPI api.UserInternalAPI + serverName spec.ServerName + cfg *config.MSC3861 + httpClient *http.Client + openIdConfig *OpenIDConfiguration + allowGuest bool +} + +func newMSC3861UserVerifier( + userAPI api.UserInternalAPI, + serverName spec.ServerName, + cfg *config.MSC3861, + allowGuest bool, +) (*MSC3861UserVerifier, error) { + openIdConfig, err := fetchOpenIDConfiguration(&http.Client{}, cfg.Issuer) + if err != nil { + return nil, err + } + return &MSC3861UserVerifier{ + userAPI: userAPI, + serverName: serverName, + cfg: cfg, + openIdConfig: openIdConfig, + allowGuest: allowGuest, + httpClient: http.DefaultClient, + }, nil +} + +type mscError struct { + Code errCode + Msg string +} + +func (r *mscError) Error() string { + return fmt.Sprintf("%s: %s", r.Code, r.Msg) +} + +// VerifyUserFromRequest authenticates the HTTP request, on success returns Device of the requester. +func (m *MSC3861UserVerifier) VerifyUserFromRequest(req *http.Request) (*api.Device, *util.JSONResponse) { + util.GetLogger(req.Context()).Debug("MSC3861.VerifyUserFromRequest") + // Try to find the Application Service user + token, err := auth.ExtractAccessToken(req) + if err != nil { + return nil, &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: spec.MissingToken(err.Error()), + } + } + // TODO: try to get appservice user first. See https://github.com/element-hq/synapse/blob/develop/synapse/api/auth/msc3861_delegated.py#L273 + userData, err := m.getUserByAccessToken(req.Context(), token) + if err != nil { + switch e := err.(type) { + case (*mscError): + switch e.Code { + case codeIntrospectionNot2xx, codeOpenidConfigDecodingFailed, codeOpenidConfigEndpointNon2xx: + return nil, &util.JSONResponse{ + Code: http.StatusServiceUnavailable, + JSON: spec.Unknown(e.Error()), + } + case codeInvalidClientToken: + return nil, &util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.Forbidden(e.Error()), + } + case codeAuthError: + return nil, &util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.Unknown(e.Error()), + } + case codeMxidError: + return nil, &util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.Unknown(e.Error()), + } + default: + r := util.ErrorResponse(err) + return nil, &r + } + default: + r := util.ErrorResponse(err) + return nil, &r + } + } + + // Do not record requests from MAS using the virtual `__oidc_admin` user. + if token != m.cfg.AdminToken { + // TODO: not sure which exact data we should record here. See the link for reference + // https://github.com/element-hq/synapse/blob/develop/synapse/api/auth/base.py#L365 + } + + if !m.allowGuest && userData.IsGuest { + return nil, &util.JSONResponse{ + Code: http.StatusUnauthorized, + JSON: spec.Forbidden(strings.Join([]string{"Insufficient scope: ", scopeMatrixAPI}, "")), + } + } + + return userData.Device, nil +} + +type requester struct { + Device *api.Device + UserID *spec.UserID + Scope []string + IsGuest bool +} + +func (m *MSC3861UserVerifier) getUserByAccessToken(ctx context.Context, token string) (*requester, error) { + var userID *spec.UserID + logger := util.GetLogger(ctx) + + if adminToken := m.cfg.AdminToken; adminToken != "" && token == adminToken { + // XXX: This is a temporary solution so that the admin API can be called by + // the OIDC provider. This will be removed once we have OIDC client + // credentials grant support in matrix-authentication-service. + logger.Info("Admin token used") + // XXX: that user doesn't exist and won't be provisioned. + adminUser, err := createUserID("__oidc_admin", m.serverName) + if err != nil { + return nil, err + } + return &requester{ + UserID: adminUser, + Scope: []string{"urn:synapse:admin:*"}, + Device: &api.Device{UserID: adminUser.Local(), AccountType: api.AccountTypeAdmin}, + }, nil + } + + introspectionResult, err := m.introspectToken(ctx, token) + if err != nil { + logger.WithError(err).Error("MSC3861UserVerifier:introspectToken") + return nil, err + } + logger.Debugf("Introspection result: %+v", *introspectionResult) + + if !introspectionResult.Active { + return nil, &mscError{Code: codeInvalidClientToken, Msg: "Token is not active"} + } + + scopes := introspectionResult.Scopes() + hasUserScope, hasGuestScope := slices.Contains(scopes, scopeMatrixAPI), slices.Contains(scopes, scopeMatrixGuest) + if !hasUserScope && !hasGuestScope { + return nil, &mscError{Code: codeInvalidClientToken, Msg: "No scope in token granting user rights"} + } + + sub := introspectionResult.Sub + if sub == "" { + return nil, &mscError{Code: codeInvalidClientToken, Msg: "Invalid sub claim in the introspection result"} + } + + localpart := "" + { + var rs api.QueryLocalpartExternalIDResponse + if err = m.userAPI.QueryExternalUserIDByLocalpartAndProvider(ctx, &api.QueryLocalpartExternalIDRequest{ + ExternalID: sub, + AuthProvider: externalAuthProvider, + }, &rs); err != nil && err != sql.ErrNoRows { + return nil, err + } + if l := rs.LocalpartExternalID; l != nil { + localpart = l.Localpart + } + } + + if localpart == "" { + // If we could not find a user via the external_id, it either does not exist, + // or the external_id was never recorded + username := introspectionResult.Username + if username == "" { + return nil, &mscError{Code: codeAuthError, Msg: "Invalid username claim in the introspection result"} + } + userID, err = createUserID(username, m.serverName) + if err != nil { + logger.WithError(err).Error("getUserByAccessToken:createUserID") + return nil, err + } + + // First try to find a user from the username claim + var account *api.Account + { + var rs api.QueryAccountByLocalpartResponse + err := m.userAPI.QueryAccountByLocalpart(ctx, &api.QueryAccountByLocalpartRequest{Localpart: userID.Local(), ServerName: userID.Domain()}, &rs) + if err != nil && err != sql.ErrNoRows { + logger.WithError(err).Error("QueryAccountByLocalpart") + return nil, err + } + account = rs.Account + } + + if account == nil { + // If the user does not exist, we should create it on the fly + var rs api.PerformAccountCreationResponse + if err = m.userAPI.PerformAccountCreation(ctx, &api.PerformAccountCreationRequest{ + AccountType: api.AccountTypeUser, + Localpart: userID.Local(), + ServerName: userID.Domain(), + }, &rs); err != nil { + logger.WithError(err).Error("PerformAccountCreation") + return nil, err + } + } + + if err := m.userAPI.PerformLocalpartExternalUserIDCreation(ctx, &api.PerformLocalpartExternalUserIDCreationRequest{ + Localpart: userID.Local(), + ExternalID: sub, + AuthProvider: externalAuthProvider, + }); err != nil { + logger.WithError(err).Error("PerformLocalpartExternalUserIDCreation") + return nil, err + } + + localpart = userID.Local() + } + + if userID == nil { + userID, err = createUserID(localpart, m.serverName) + if err != nil { + logger.WithError(err).Error("getUserByAccessToken:createUserID") + return nil, err + } + } + + deviceIDs := make([]string, 0, 1) + for i := range scopes { + if s := scopes[i]; strings.HasPrefix(s, scopeMatrixDevicePrefix) { + deviceIDs = append(deviceIDs, s[len(scopeMatrixDevicePrefix):]) + } + } + + if len(deviceIDs) != 1 { + logger.Errorf("Invalid device IDs in scope: %+v", deviceIDs) + return nil, &mscError{Code: codeAuthError, Msg: "Invalid device IDs in scope"} + } + + var device *api.Device + + deviceID := deviceIDs[0] + if len(deviceID) > 255 || len(deviceID) < 1 { + return nil, &mscError{ + Code: codeAuthError, + Msg: strings.Join([]string{"Invalid device ID in scope: ", deviceID}, ""), + } + } + logger.Debugf("deviceID is: %s", deviceID) + logger.Debugf("scope is: %+v", scopes) + + userDeviceExists := false + { + var rs api.QueryDevicesResponse + err := m.userAPI.QueryDevices(ctx, &api.QueryDevicesRequest{UserID: userID.String()}, &rs) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + for i := range rs.Devices { + if d := &rs.Devices[i]; d.ID == deviceID { + userDeviceExists = true + device = d + break + } + } + } + logger.Debugf("userDeviceExists is: %t", userDeviceExists) + if !userDeviceExists { + var rs api.PerformDeviceCreationResponse + deviceDisplayName := "OIDC-native client" + if err := m.userAPI.PerformDeviceCreation(ctx, &api.PerformDeviceCreationRequest{ + Localpart: localpart, + ServerName: m.serverName, + AccessToken: token, + DeviceID: &deviceID, + DeviceDisplayName: &deviceDisplayName, + // TODO: Cannot add IPAddr and Useragent values here. Should we care about it here? + }, &rs); err != nil { + logger.WithError(err).Error("PerformDeviceCreation") + return nil, err + } + device = rs.Device + logger.Debugf("PerformDeviceCreationResponse is: %+v", rs) + } + + + + return &requester{ + Device: device, + UserID: userID, + Scope: scopes, + IsGuest: hasGuestScope && !hasUserScope, + }, nil +} + +func createUserID(local string, serverName spec.ServerName) (*spec.UserID, error) { + userID, err := spec.NewUserID(strings.Join([]string{"@", local, ":", string(serverName)}, ""), false) + if err != nil { + return nil, &mscError{Code: codeMxidError, Msg: err.Error()} + } + return userID, nil +} + +func (m *MSC3861UserVerifier) introspectToken(ctx context.Context, token string) (*introspectionResponse, error) { + formBody := url.Values{"token": []string{token}} + encoded := formBody.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, m.openIdConfig.IntrospectionEndpoint, strings.NewReader(encoded)) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(m.cfg.ClientID, m.cfg.ClientSecret) + + resp, err := m.httpClient.Do(req) + if err != nil { + return nil, err + } + body := resp.Body + defer resp.Body.Close() + + if c := resp.StatusCode; c < 200 || c >= 300 { + return nil, errors.New(strings.Join([]string{"The introspection endpoint returned a '", resp.Status ,"' response"}, "")) + } + var ir introspectionResponse + if err := json.NewDecoder(body).Decode(&ir); err != nil { + return nil, err + } + return &ir, nil +} + +type OpenIDConfiguration struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + JWKsURI string `json:"jwks_uri"` + RegistrationEndpoint string `json:"registration_endpoint"` + ScopesSupported []string `json:"scopes_supported"` + ResponseTypesSupported []string `json:"response_types_supported"` + ResponseModesSupported []string `json:"response_modes_supported"` + GrantTypesSupported []string `json:"grant_types_supported"` + TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` + TokenEndpointAuthSigningAlgCaluesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported"` + RevocationEnpoint string `json:"revocation_endpoint"` + RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported"` + RevocationEndpointAuthSigningAlgValues []string `json:"revocation_endpoint_auth_signing_alg_values_supported"` + IntrospectionEndpoint string `json:"introspection_endpoint"` + IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported"` + IntrospectionEndpointAuthSigningAlgValues []string `json:"introspection_endpoint_auth_signing_alg_values_supported"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + UserinfoEndpoint string `json:"userinfo_endpoint"` + SubjectTypesSupported []string `json:"subject_types_supported"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported"` + DisplayValuesSupported []string `json:"display_values_supported"` + ClaimTypesSupported []string `json:"claim_types_supported"` + ClaimsSupported []string `json:"claims_supported"` + ClaimsParameterSupported bool `json:"claims_parameter_supported"` + RequestParameterSupported bool `json:"request_parameter_supported"` + RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` + PromptValuesSupported []string `json:"prompt_values_supported"` + DeviceAuthorizaEndpoint string `json:"device_authorization_endpoint"` + AccountManagementURI string `json:"account_management_uri"` + AccountManagementActionsSupported []string `json:"account_management_actions_supported"` +} + +func fetchOpenIDConfiguration(httpClient *http.Client, authHostURL string) (* + OpenIDConfiguration, error) { + u, err := url.Parse(authHostURL) + if err != nil { + return nil, err + } + u = u.JoinPath(".well-known/openid-configuration") + resp, err := httpClient.Get(u.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, &mscError{Code: codeOpenidConfigEndpointNon2xx, Msg: ".well-known/openid-configuration endpoint returned non-200 response"} + } + var oic OpenIDConfiguration + if err := json.NewDecoder(resp.Body).Decode(&oic); err != nil { + return nil, &mscError{Code: codeOpenidConfigDecodingFailed, Msg: err.Error()} + } + return &oic, nil +} + +// introspectionResponse as described in the RFC https://datatracker.ietf.org/doc/html/rfc7662#section-2.2 +type introspectionResponse struct { + Active bool `json:"active"` // required + Scope string `json:"scope"` // optional + Username string `json:"username"` // optional + TokenType string `json:"token_type"` // optional + Exp *int64 `json:"exp"` // optional + Iat *int64 `json:"iat"` // optional + Nfb *int64 `json:"nfb"` // optional + Sub string `json:"sub"` // optional + Jti string `json:"jti"` // optional + Aud string `json:"aud"` // optional + Iss string `json:"iss"` // optional +} + +func (i *introspectionResponse) Scopes() []string { + return strings.Split(i.Scope, " ") +} diff --git a/setup/mscs/mscs.go b/setup/mscs/mscs.go index 8df539ba..3881b8e0 100644 --- a/setup/mscs/mscs.go +++ b/setup/mscs/mscs.go @@ -16,6 +16,7 @@ import ( "github.com/element-hq/dendrite/setup" "github.com/element-hq/dendrite/setup/config" "github.com/element-hq/dendrite/setup/mscs/msc2836" + "github.com/element-hq/dendrite/setup/mscs/msc3861" "github.com/matrix-org/util" "github.com/sirupsen/logrus" ) @@ -34,10 +35,11 @@ func Enable(cfg *config.Dendrite, cm *sqlutil.Connections, routers httputil.Rout func EnableMSC(cfg *config.Dendrite, cm *sqlutil.Connections, routers httputil.Routers, monolith *setup.Monolith, msc string, caches *caching.Caches) error { switch msc { case "msc2836": - return msc2836.Enable(cfg, cm, routers, monolith.RoomserverAPI, monolith.FederationAPI, monolith.UserAPI, monolith.KeyRing) + return msc2836.Enable(cfg, cm, routers, monolith.RoomserverAPI, monolith.FederationAPI, monolith.UserVerifierProvider, monolith.KeyRing) case "msc2444": // enabled inside federationapi case "msc2753": // enabled inside clientapi - case "msc3861": // enabled inside clientapi + case "msc3861": + return msc3861.Enable(monolith) default: logrus.Warnf("EnableMSC: unknown MSC '%s', this MSC is either not supported or is natively supported by Dendrite", msc) } diff --git a/syncapi/routing/routing.go b/syncapi/routing/routing.go index dcc78c85..48473698 100644 --- a/syncapi/routing/routing.go +++ b/syncapi/routing/routing.go @@ -36,16 +36,17 @@ func Setup( lazyLoadCache caching.LazyLoadCache, fts fulltext.Indexer, rateLimits *httputil.RateLimits, + userVerifier httputil.UserVerifier, ) { v1unstablemux := csMux.PathPrefix("/{apiversion:(?:v1|unstable)}/").Subrouter() v3mux := csMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter() // TODO: Add AS support for all handlers below. - v3mux.Handle("/sync", httputil.MakeAuthAPI("sync", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + v3mux.Handle("/sync", httputil.MakeAuthAPI("sync", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return srp.OnIncomingSyncRequest(req, device) }, httputil.WithAllowGuests())).Methods(http.MethodGet, http.MethodOptions) - v3mux.Handle("/rooms/{roomID}/messages", httputil.MakeAuthAPI("room_messages", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + v3mux.Handle("/rooms/{roomID}/messages", httputil.MakeAuthAPI("room_messages", userVerifier, 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 @@ -58,7 +59,7 @@ func Setup( }, httputil.WithAllowGuests())).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/event/{eventID}", - httputil.MakeAuthAPI("rooms_get_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_get_event", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -68,7 +69,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/user/{userId}/filter", - httputil.MakeAuthAPI("put_filter", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("put_filter", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -78,7 +79,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/user/{userId}/filter/{filterId}", - httputil.MakeAuthAPI("get_filter", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("get_filter", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -87,12 +88,12 @@ func Setup( }), ).Methods(http.MethodGet, http.MethodOptions) - v3mux.Handle("/keys/changes", httputil.MakeAuthAPI("keys_changes", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + v3mux.Handle("/keys/changes", httputil.MakeAuthAPI("keys_changes", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { return srp.OnIncomingKeyChangeRequest(req, device) }, httputil.WithAllowGuests())).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/rooms/{roomId}/context/{eventId}", - httputil.MakeAuthAPI("context", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("context", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -108,7 +109,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v1unstablemux.Handle("/rooms/{roomId}/relations/{eventId}", - httputil.MakeAuthAPI("relations", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("relations", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -122,7 +123,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v1unstablemux.Handle("/rooms/{roomId}/relations/{eventId}/{relType}", - httputil.MakeAuthAPI("relation_type", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("relation_type", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -136,7 +137,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v1unstablemux.Handle("/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}", - httputil.MakeAuthAPI("relation_type_event", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("relation_type_event", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) @@ -150,7 +151,7 @@ func Setup( ).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/search", - httputil.MakeAuthAPI("search", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("search", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { if !cfg.Fulltext.Enabled { return util.JSONResponse{ Code: http.StatusNotImplemented, @@ -173,7 +174,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/members", - httputil.MakeAuthAPI("rooms_members", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + httputil.MakeAuthAPI("rooms_members", userVerifier, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) if err != nil { return util.ErrorResponse(err) diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index 2b1dc995..a45173db 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -42,6 +42,7 @@ func AddPublicRoutes( userAPI userapi.SyncUserAPI, rsAPI api.SyncRoomserverAPI, caches caching.LazyLoadCache, + userVerifier httputil.UserVerifier, enableMetrics bool, ) { js, natsClient := natsInstance.Prepare(processContext, &dendriteCfg.Global.JetStream) @@ -149,5 +150,6 @@ func AddPublicRoutes( routers.Client, requestPool, syncDB, userAPI, rsAPI, &dendriteCfg.SyncAPI, caches, fts, rateLimits, + userVerifier, ) } diff --git a/userapi/api/api.go b/userapi/api/api.go index f0ef26bf..6899e5e2 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -32,6 +32,8 @@ type UserInternalAPI interface { QuerySearchProfilesAPI // used by p2p demos QueryAccountByLocalpart(ctx context.Context, req *QueryAccountByLocalpartRequest, res *QueryAccountByLocalpartResponse) (err error) + QueryExternalUserIDByLocalpartAndProvider(ctx context.Context, req *QueryLocalpartExternalIDRequest, res *QueryLocalpartExternalIDResponse) (err error) + PerformLocalpartExternalUserIDCreation(ctx context.Context, req *PerformLocalpartExternalUserIDCreationRequest) (err error) } // api functions required by the appservice api @@ -129,6 +131,7 @@ type QuerySearchProfilesAPI interface { QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error } +// FIXME: typo in Acccess // common function for creating authenticated endpoints (used in client/media/sync api) type QueryAcccessTokenAPI interface { QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error @@ -316,6 +319,9 @@ type PerformAccountCreationRequest struct { Localpart string // Required: The localpart for this account. Ignored if account type is guest. ServerName spec.ServerName // optional: if not specified, default server name used instead + DisplayName string // optional: this is populated only by MAS. In the legacy flow it's not used + AvatarURL string // optional: this is populated only by MAS. In the legacy flow it's not used + AppServiceID string // optional: the application service ID (not user ID) creating this account, if any. Password string // optional: if missing then this account will be a passwordless account OnConflict Conflict @@ -653,10 +659,26 @@ type QueryAccountByLocalpartResponse struct { Account *Account } +type QueryLocalpartExternalIDRequest struct { + ExternalID string + AuthProvider string +} + +type QueryLocalpartExternalIDResponse struct { + LocalpartExternalID *LocalpartExternalID +} + +type PerformLocalpartExternalUserIDCreationRequest struct { + Localpart string + ExternalID string + AuthProvider string +} + // API functions required by the clientapi type ClientKeyAPI interface { UploadDeviceKeysAPI QueryKeys(ctx context.Context, req *QueryKeysRequest, res *QueryKeysResponse) + QueryMasterKeys(ctx context.Context, req *QueryMasterKeysRequest, res *QueryMasterKeysResponse) PerformUploadKeys(ctx context.Context, req *PerformUploadKeysRequest, res *PerformUploadKeysResponse) error PerformUploadDeviceSignatures(ctx context.Context, req *PerformUploadDeviceSignaturesRequest, res *PerformUploadDeviceSignaturesResponse) @@ -918,6 +940,16 @@ type QueryKeysResponse struct { Error *KeyError } +type QueryMasterKeysRequest struct { + UserID string +} + +type QueryMasterKeysResponse struct { + Key *types.CrossSigningKey + // Set if there was a fatal error processing this query + Error *KeyError +} + type QueryKeyChangesRequest struct { // The offset of the last received key event, or sarama.OffsetOldest if this is from the beginning Offset int64 diff --git a/userapi/internal/cross_signing.go b/userapi/internal/cross_signing.go index fe5d9f7d..dfd426c3 100644 --- a/userapi/internal/cross_signing.go +++ b/userapi/internal/cross_signing.go @@ -114,7 +114,9 @@ func (a *UserInternalAPI) PerformUploadDeviceKeys(ctx context.Context, req *api. byPurpose[fclient.CrossSigningKeyPurposeMaster] = req.MasterKey for _, key := range req.MasterKey.Keys { // iterates once, see sanityCheckKey - toStore[fclient.CrossSigningKeyPurposeMaster] = key + toStore[fclient.CrossSigningKeyPurposeMaster] = types.CrossSigningKey{ + KeyData: key, + } } hasMasterKey = true } @@ -130,7 +132,9 @@ func (a *UserInternalAPI) PerformUploadDeviceKeys(ctx context.Context, req *api. byPurpose[fclient.CrossSigningKeyPurposeSelfSigning] = req.SelfSigningKey for _, key := range req.SelfSigningKey.Keys { // iterates once, see sanityCheckKey - toStore[fclient.CrossSigningKeyPurposeSelfSigning] = key + toStore[fclient.CrossSigningKeyPurposeSelfSigning] = types.CrossSigningKey{ + KeyData: key, + } } } @@ -145,7 +149,9 @@ func (a *UserInternalAPI) PerformUploadDeviceKeys(ctx context.Context, req *api. byPurpose[fclient.CrossSigningKeyPurposeUserSigning] = req.UserSigningKey for _, key := range req.UserSigningKey.Keys { // iterates once, see sanityCheckKey - toStore[fclient.CrossSigningKeyPurposeUserSigning] = key + toStore[fclient.CrossSigningKeyPurposeUserSigning] = types.CrossSigningKey{ + KeyData: key, + } } } @@ -198,7 +204,7 @@ func (a *UserInternalAPI) PerformUploadDeviceKeys(ctx context.Context, req *api. changed = true break } - if !bytes.Equal(old, new) { + if !bytes.Equal(old.KeyData, new.KeyData) { // One of the existing keys for a purpose we already knew about has // changed. changed = true @@ -210,7 +216,7 @@ func (a *UserInternalAPI) PerformUploadDeviceKeys(ctx context.Context, req *api. } // Store the keys. - if err := a.KeyDatabase.StoreCrossSigningKeysForUser(ctx, req.UserID, toStore); err != nil { + if err := a.KeyDatabase.StoreCrossSigningKeysForUser(ctx, req.UserID, toStore, nil); err != nil { res.Error = &api.KeyError{ Err: fmt.Sprintf("a.DB.StoreCrossSigningKeysForUser: %s", err), } diff --git a/userapi/internal/key_api.go b/userapi/internal/key_api.go index 6cb11bcd..98c38784 100644 --- a/userapi/internal/key_api.go +++ b/userapi/internal/key_api.go @@ -234,6 +234,19 @@ func (a *UserInternalAPI) PerformMarkAsStaleIfNeeded(ctx context.Context, req *a return a.Updater.ManualUpdate(ctx, req.Domain, req.UserID) } +func (a *UserInternalAPI) QueryMasterKeys(ctx context.Context, req *api.QueryMasterKeysRequest, res *api.QueryMasterKeysResponse) { + crossSigningKeyMap, err := a.KeyDatabase.CrossSigningKeysDataForUserAndKeyType(ctx, req.UserID, fclient.CrossSigningKeyPurposeMaster) + if err != nil { + res.Error = &api.KeyError{ + Err: fmt.Sprintf("failed to query user cross signing master keys: %s", err), + } + return + } + if key, ok := crossSigningKeyMap[fclient.CrossSigningKeyPurposeMaster]; ok { + res.Key = &key + } +} + // nolint:gocyclo func (a *UserInternalAPI) QueryKeys(ctx context.Context, req *api.QueryKeysRequest, res *api.QueryKeysResponse) { var respMu sync.Mutex diff --git a/userapi/internal/user_api.go b/userapi/internal/user_api.go index 666e75f9..a7760c1b 100644 --- a/userapi/internal/user_api.go +++ b/userapi/internal/user_api.go @@ -7,6 +7,7 @@ package internal import ( + "cmp" "context" "database/sql" "encoding/json" @@ -247,10 +248,17 @@ func (a *UserInternalAPI) PerformAccountCreation(ctx context.Context, req *api.P return nil } - if _, _, err = a.DB.SetDisplayName(ctx, req.Localpart, serverName, req.Localpart); err != nil { + displayName := cmp.Or(req.DisplayName, req.Localpart) + if _, _, err = a.DB.SetDisplayName(ctx, req.Localpart, serverName, displayName); err != nil { return fmt.Errorf("a.DB.SetDisplayName: %w", err) } + if req.AvatarURL != "" { + if _, _, err := a.DB.SetAvatarURL(ctx, req.Localpart, serverName, req.AvatarURL); err != nil { + return fmt.Errorf("a.DB.SetAvatarURL: %w", err) + } + } + postRegisterJoinRooms(a.Config, acc, a.RSAPI) res.AccountCreated = true @@ -594,6 +602,15 @@ func (a *UserInternalAPI) QueryAccountByLocalpart(ctx context.Context, req *api. return } +func (a *UserInternalAPI) PerformLocalpartExternalUserIDCreation(ctx context.Context, req *api.PerformLocalpartExternalUserIDCreationRequest) (err error) { + return a.DB.CreateLocalpartExternalID(ctx, req.Localpart, req.ExternalID, req.AuthProvider) +} + +func (a *UserInternalAPI) QueryExternalUserIDByLocalpartAndProvider(ctx context.Context, req *api.QueryLocalpartExternalIDRequest, res *api.QueryLocalpartExternalIDResponse) (err error) { + res.LocalpartExternalID, err = a.DB.GetLocalpartForExternalID(ctx, req.ExternalID, req.AuthProvider) + return +} + // Return the appservice 'device' or nil if the token is not an appservice. Returns an error if there was a problem // creating a 'device'. func (a *UserInternalAPI) queryAppServiceToken(ctx context.Context, token, appServiceUserID string) (*api.Device, error) { diff --git a/userapi/storage/interface.go b/userapi/storage/interface.go index 13d8c201..6dbf97c5 100644 --- a/userapi/storage/interface.go +++ b/userapi/storage/interface.go @@ -226,9 +226,10 @@ type KeyDatabase interface { CrossSigningKeysForUser(ctx context.Context, userID string) (map[fclient.CrossSigningKeyPurpose]fclient.CrossSigningKey, error) CrossSigningKeysDataForUser(ctx context.Context, userID string) (types.CrossSigningKeyMap, error) + CrossSigningKeysDataForUserAndKeyType(ctx context.Context, userID string, keyType fclient.CrossSigningKeyPurpose) (types.CrossSigningKeyMap, error) CrossSigningSigsForTarget(ctx context.Context, originUserID, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (types.CrossSigningSigMap, error) - StoreCrossSigningKeysForUser(ctx context.Context, userID string, keyMap types.CrossSigningKeyMap) error + StoreCrossSigningKeysForUser(ctx context.Context, userID string, keyMap types.CrossSigningKeyMap, updatableWithoutUIABeforeMs *int64) error StoreCrossSigningSigsForTarget(ctx context.Context, originUserID string, originKeyID gomatrixserverlib.KeyID, targetUserID string, targetKeyID gomatrixserverlib.KeyID, signature spec.Base64Bytes) error DeleteStaleDeviceLists( diff --git a/userapi/storage/postgres/cross_signing_keys_table.go b/userapi/storage/postgres/cross_signing_keys_table.go index a8566e69..a8f0d8cb 100644 --- a/userapi/storage/postgres/cross_signing_keys_table.go +++ b/userapi/storage/postgres/cross_signing_keys_table.go @@ -24,23 +24,29 @@ CREATE TABLE IF NOT EXISTS keyserver_cross_signing_keys ( user_id TEXT NOT NULL, key_type SMALLINT NOT NULL, key_data TEXT NOT NULL, + updatable_without_uia_before_ms BIGINT DEFAULT NULL, PRIMARY KEY (user_id, key_type) ); ` const selectCrossSigningKeysForUserSQL = "" + - "SELECT key_type, key_data FROM keyserver_cross_signing_keys" + + "SELECT key_type, key_data, updatable_without_uia_before_ms FROM keyserver_cross_signing_keys" + " WHERE user_id = $1" +const selectCrossSigningKeysForUserAndKeyTypeSQL = "" + + "SELECT key_type, key_data, updatable_without_uia_before_ms FROM keyserver_cross_signing_keys" + + " WHERE user_id = $1 AND key_type = $2" + const upsertCrossSigningKeysForUserSQL = "" + "INSERT INTO keyserver_cross_signing_keys (user_id, key_type, key_data)" + " VALUES($1, $2, $3)" + " ON CONFLICT (user_id, key_type) DO UPDATE SET key_data = $3" type crossSigningKeysStatements struct { - db *sql.DB - selectCrossSigningKeysForUserStmt *sql.Stmt - upsertCrossSigningKeysForUserStmt *sql.Stmt + db *sql.DB + selectCrossSigningKeysForUserStmt *sql.Stmt + selectCrossSigningKeysForUserAndKeyTypeStmt *sql.Stmt + upsertCrossSigningKeysForUserStmt *sql.Stmt } func NewPostgresCrossSigningKeysTable(db *sql.DB) (tables.CrossSigningKeys, error) { @@ -53,6 +59,7 @@ func NewPostgresCrossSigningKeysTable(db *sql.DB) (tables.CrossSigningKeys, erro } return s, sqlutil.StatementList{ {&s.selectCrossSigningKeysForUserStmt, selectCrossSigningKeysForUserSQL}, + {&s.selectCrossSigningKeysForUserAndKeyTypeStmt, selectCrossSigningKeysForUserAndKeyTypeSQL}, {&s.upsertCrossSigningKeysForUserStmt, upsertCrossSigningKeysForUserSQL}, }.Prepare(db) } @@ -69,27 +76,64 @@ func (s *crossSigningKeysStatements) SelectCrossSigningKeysForUser( for rows.Next() { var keyTypeInt int16 var keyData spec.Base64Bytes - if err = rows.Scan(&keyTypeInt, &keyData); err != nil { + var updatableWithoutUIABeforeMs *int64 + if err = rows.Scan(&keyTypeInt, &keyData, &updatableWithoutUIABeforeMs); err != nil { return nil, err } keyType, ok := types.KeyTypeIntToPurpose[keyTypeInt] if !ok { return nil, fmt.Errorf("unknown key purpose int %d", keyTypeInt) } - r[keyType] = keyData + r[keyType] = types.CrossSigningKey{ + UpdatableWithoutUIABeforeMs: updatableWithoutUIABeforeMs, + KeyData: keyData, + } + } + err = rows.Err() + return +} + +func (s *crossSigningKeysStatements) SelectCrossSigningKeysForUserAndKeyType( + ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, +) (r types.CrossSigningKeyMap, err error) { + keyTypeInt, ok := types.KeyTypePurposeToInt[keyType] + if !ok { + return nil, fmt.Errorf("unknown key purpose %q", keyType) + } + rows, err := sqlutil.TxStmt(txn, s.selectCrossSigningKeysForUserAndKeyTypeStmt).QueryContext(ctx, userID, keyTypeInt) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "SelectCrossSigningKeysForUserAndKeyType: rows.close() failed") + r = types.CrossSigningKeyMap{} + for rows.Next() { + var keyTypeInt int16 + var keyData spec.Base64Bytes + var updatableWithoutUIABeforeMs *int64 + if err = rows.Scan(&keyTypeInt, &keyData, &updatableWithoutUIABeforeMs); err != nil { + return nil, err + } + keyType, ok := types.KeyTypeIntToPurpose[keyTypeInt] + if !ok { + return nil, fmt.Errorf("unknown key purpose int %d", keyTypeInt) + } + r[keyType] = types.CrossSigningKey{ + UpdatableWithoutUIABeforeMs: updatableWithoutUIABeforeMs, + KeyData: keyData, + } } err = rows.Err() return } func (s *crossSigningKeysStatements) UpsertCrossSigningKeysForUser( - ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, keyData spec.Base64Bytes, + ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, keyData spec.Base64Bytes, updatableWithoutUIABeforeMs *int64, ) error { keyTypeInt, ok := types.KeyTypePurposeToInt[keyType] if !ok { return fmt.Errorf("unknown key purpose %q", keyType) } - if _, err := sqlutil.TxStmt(txn, s.upsertCrossSigningKeysForUserStmt).ExecContext(ctx, userID, keyTypeInt, keyData); err != nil { + if _, err := sqlutil.TxStmt(txn, s.upsertCrossSigningKeysForUserStmt).ExecContext(ctx, userID, keyTypeInt, keyData, updatableWithoutUIABeforeMs); err != nil { return fmt.Errorf("s.upsertCrossSigningKeysForUserStmt: %w", err) } return nil diff --git a/userapi/storage/shared/storage.go b/userapi/storage/shared/storage.go index aade4be1..80b225c9 100644 --- a/userapi/storage/shared/storage.go +++ b/userapi/storage/shared/storage.go @@ -1101,12 +1101,12 @@ func (d *KeyDatabase) CrossSigningKeysForUser(ctx context.Context, userID string } results := map[fclient.CrossSigningKeyPurpose]fclient.CrossSigningKey{} for purpose, key := range keyMap { - keyID := gomatrixserverlib.KeyID("ed25519:" + key.Encode()) + keyID := gomatrixserverlib.KeyID("ed25519:" + key.KeyData.Encode()) result := fclient.CrossSigningKey{ UserID: userID, Usage: []fclient.CrossSigningKeyPurpose{purpose}, Keys: map[gomatrixserverlib.KeyID]spec.Base64Bytes{ - keyID: key, + keyID: key.KeyData, }, } sigMap, err := d.CrossSigningSigsTable.SelectCrossSigningSigsForTarget(ctx, nil, userID, userID, keyID) @@ -1137,16 +1137,21 @@ func (d *KeyDatabase) CrossSigningKeysDataForUser(ctx context.Context, userID st return d.CrossSigningKeysTable.SelectCrossSigningKeysForUser(ctx, nil, userID) } +// CrossSigningKeysForUserAndKeyType returns the latest known cross-signing keys for a user and key type, if any. +func (d *KeyDatabase) CrossSigningKeysDataForUserAndKeyType(ctx context.Context, userID string, keyType fclient.CrossSigningKeyPurpose) (types.CrossSigningKeyMap, error) { + return d.CrossSigningKeysTable.SelectCrossSigningKeysForUserAndKeyType(ctx, nil, userID, keyType) +} + // CrossSigningSigsForTarget returns the signatures for a given user's key ID, if any. func (d *KeyDatabase) CrossSigningSigsForTarget(ctx context.Context, originUserID, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (types.CrossSigningSigMap, error) { return d.CrossSigningSigsTable.SelectCrossSigningSigsForTarget(ctx, nil, originUserID, targetUserID, targetKeyID) } // StoreCrossSigningKeysForUser stores the latest known cross-signing keys for a user. -func (d *KeyDatabase) StoreCrossSigningKeysForUser(ctx context.Context, userID string, keyMap types.CrossSigningKeyMap) error { +func (d *KeyDatabase) StoreCrossSigningKeysForUser(ctx context.Context, userID string, keyMap types.CrossSigningKeyMap, updatableWithoutUIABeforeMs *int64) error { return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { - for keyType, keyData := range keyMap { - if err := d.CrossSigningKeysTable.UpsertCrossSigningKeysForUser(ctx, txn, userID, keyType, keyData); err != nil { + for keyType, key := range keyMap { + if err := d.CrossSigningKeysTable.UpsertCrossSigningKeysForUser(ctx, txn, userID, keyType, key.KeyData, key.UpdatableWithoutUIABeforeMs); err != nil { return fmt.Errorf("d.CrossSigningKeysTable.InsertCrossSigningKeysForUser: %w", err) } } diff --git a/userapi/storage/sqlite3/cross_signing_keys_table.go b/userapi/storage/sqlite3/cross_signing_keys_table.go index dd8923d3..9c4d3cb5 100644 --- a/userapi/storage/sqlite3/cross_signing_keys_table.go +++ b/userapi/storage/sqlite3/cross_signing_keys_table.go @@ -24,22 +24,28 @@ CREATE TABLE IF NOT EXISTS keyserver_cross_signing_keys ( user_id TEXT NOT NULL, key_type INTEGER NOT NULL, key_data TEXT NOT NULL, + updatable_without_uia_before_ms BIGINT DEFAULT NULL, PRIMARY KEY (user_id, key_type) ); ` const selectCrossSigningKeysForUserSQL = "" + - "SELECT key_type, key_data FROM keyserver_cross_signing_keys" + + "SELECT key_type, key_data, updatable_without_uia_before_ms FROM keyserver_cross_signing_keys" + " WHERE user_id = $1" +const selectCrossSigningKeysForUserAndKeyTypeSQL = "" + + "SELECT key_type, key_data, updatable_without_uia_before_ms FROM keyserver_cross_signing_keys" + + " WHERE user_id = $1 AND key_type = $2" + const upsertCrossSigningKeysForUserSQL = "" + - "INSERT OR REPLACE INTO keyserver_cross_signing_keys (user_id, key_type, key_data)" + - " VALUES($1, $2, $3)" + "INSERT OR REPLACE INTO keyserver_cross_signing_keys (user_id, key_type, key_data, updatable_without_uia_before_ms)" + + " VALUES($1, $2, $3, $4)" type crossSigningKeysStatements struct { - db *sql.DB - selectCrossSigningKeysForUserStmt *sql.Stmt - upsertCrossSigningKeysForUserStmt *sql.Stmt + db *sql.DB + selectCrossSigningKeysForUserStmt *sql.Stmt + selectCrossSigningKeysForUserAndKeyTypeStmt *sql.Stmt + upsertCrossSigningKeysForUserStmt *sql.Stmt } func NewSqliteCrossSigningKeysTable(db *sql.DB) (tables.CrossSigningKeys, error) { @@ -52,6 +58,7 @@ func NewSqliteCrossSigningKeysTable(db *sql.DB) (tables.CrossSigningKeys, error) } return s, sqlutil.StatementList{ {&s.selectCrossSigningKeysForUserStmt, selectCrossSigningKeysForUserSQL}, + {&s.selectCrossSigningKeysForUserAndKeyTypeStmt, selectCrossSigningKeysForUserAndKeyTypeSQL}, {&s.upsertCrossSigningKeysForUserStmt, upsertCrossSigningKeysForUserSQL}, }.Prepare(db) } @@ -68,27 +75,64 @@ func (s *crossSigningKeysStatements) SelectCrossSigningKeysForUser( for rows.Next() { var keyTypeInt int16 var keyData spec.Base64Bytes - if err = rows.Scan(&keyTypeInt, &keyData); err != nil { + var updatableWithoutUiaBeforeMs *int64 + if err = rows.Scan(&keyTypeInt, &keyData, &updatableWithoutUiaBeforeMs); err != nil { return nil, err } keyType, ok := types.KeyTypeIntToPurpose[keyTypeInt] if !ok { return nil, fmt.Errorf("unknown key purpose int %d", keyTypeInt) } - r[keyType] = keyData + r[keyType] = types.CrossSigningKey{ + UpdatableWithoutUIABeforeMs: updatableWithoutUiaBeforeMs, + KeyData: keyData, + } + } + err = rows.Err() + return +} + +func (s *crossSigningKeysStatements) SelectCrossSigningKeysForUserAndKeyType( + ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, +) (r types.CrossSigningKeyMap, err error) { + keyTypeInt, ok := types.KeyTypePurposeToInt[keyType] + if !ok { + return nil, fmt.Errorf("unknown key purpose %q", keyType) + } + rows, err := sqlutil.TxStmt(txn, s.selectCrossSigningKeysForUserAndKeyTypeStmt).QueryContext(ctx, userID, keyTypeInt) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "SelectCrossSigningKeysForUserAndKeyType: rows.close() failed") + r = types.CrossSigningKeyMap{} + for rows.Next() { + var keyTypeInt int16 + var keyData spec.Base64Bytes + var updatableWithoutUIABeforeMs *int64 + if err = rows.Scan(&keyTypeInt, &keyData, &updatableWithoutUIABeforeMs); err != nil { + return nil, err + } + keyType, ok := types.KeyTypeIntToPurpose[keyTypeInt] + if !ok { + return nil, fmt.Errorf("unknown key purpose int %d", keyTypeInt) + } + r[keyType] = types.CrossSigningKey{ + UpdatableWithoutUIABeforeMs: updatableWithoutUIABeforeMs, + KeyData: keyData, + } } err = rows.Err() return } func (s *crossSigningKeysStatements) UpsertCrossSigningKeysForUser( - ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, keyData spec.Base64Bytes, + ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, keyData spec.Base64Bytes, updatableWithoutUIABeforeMs *int64, ) error { keyTypeInt, ok := types.KeyTypePurposeToInt[keyType] if !ok { return fmt.Errorf("unknown key purpose %q", keyType) } - if _, err := sqlutil.TxStmt(txn, s.upsertCrossSigningKeysForUserStmt).ExecContext(ctx, userID, keyTypeInt, keyData); err != nil { + if _, err := sqlutil.TxStmt(txn, s.upsertCrossSigningKeysForUserStmt).ExecContext(ctx, userID, keyTypeInt, keyData, updatableWithoutUIABeforeMs); err != nil { return fmt.Errorf("s.upsertCrossSigningKeysForUserStmt: %w", err) } return nil diff --git a/userapi/storage/tables/interface.go b/userapi/storage/tables/interface.go index 7b141629..cfd1e571 100644 --- a/userapi/storage/tables/interface.go +++ b/userapi/storage/tables/interface.go @@ -133,10 +133,6 @@ type LocalpartExternalIDsTable interface { Delete(ctx context.Context, txn *sql.Tx, externalID, authProvider string) error } -type UIAuthSessionsTable interface { - SelectByID(ctx context.Context, txn *sql.Tx, sessionID int) (*api.UIAuthSession, error) -} - type NotificationFilter uint32 const ( @@ -202,7 +198,8 @@ type StaleDeviceLists interface { type CrossSigningKeys interface { SelectCrossSigningKeysForUser(ctx context.Context, txn *sql.Tx, userID string) (r types.CrossSigningKeyMap, err error) - UpsertCrossSigningKeysForUser(ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, keyData spec.Base64Bytes) error + SelectCrossSigningKeysForUserAndKeyType(ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose) (r types.CrossSigningKeyMap, err error) + UpsertCrossSigningKeysForUser(ctx context.Context, txn *sql.Tx, userID string, keyType fclient.CrossSigningKeyPurpose, keyData spec.Base64Bytes, updatableWithoutUIABeforeMs *int64) error } type CrossSigningSigs interface { diff --git a/userapi/types/storage.go b/userapi/types/storage.go index 971f3dc9..260e2a25 100644 --- a/userapi/types/storage.go +++ b/userapi/types/storage.go @@ -37,8 +37,13 @@ var KeyTypeIntToPurpose = map[int16]fclient.CrossSigningKeyPurpose{ 3: fclient.CrossSigningKeyPurposeUserSigning, } +type CrossSigningKey struct { + UpdatableWithoutUIABeforeMs *int64 + KeyData spec.Base64Bytes +} + // Map of purpose -> public key -type CrossSigningKeyMap map[fclient.CrossSigningKeyPurpose]spec.Base64Bytes +type CrossSigningKeyMap map[fclient.CrossSigningKeyPurpose]CrossSigningKey // Map of user ID -> key ID -> signature type CrossSigningSigMap map[string]map[gomatrixserverlib.KeyID]spec.Base64Bytes