From bab4d1401fefa09294869295105da3c821bbfca7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 10 Aug 2018 07:47:14 -0700 Subject: [PATCH] AppServices: Implement /users/{userID} (#521) * Add support for querying /users/ on appservices * Fix copy/paste error --- .../dendrite/appservice/api/query.go | 95 ++++++++++++++++++- .../dendrite/appservice/query/query.go | 80 ++++++++++++++++ 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/src/github.com/matrix-org/dendrite/appservice/api/query.go b/src/github.com/matrix-org/dendrite/appservice/api/query.go index 62f61c0a..9ec21448 100644 --- a/src/github.com/matrix-org/dendrite/appservice/api/query.go +++ b/src/github.com/matrix-org/dendrite/appservice/api/query.go @@ -19,8 +19,14 @@ package api import ( "context" + "database/sql" + "errors" "net/http" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + "github.com/matrix-org/gomatrixserverlib" + commonHTTP "github.com/matrix-org/dendrite/common/http" opentracing "github.com/opentracing/opentracing-go" ) @@ -38,6 +44,27 @@ type RoomAliasExistsResponse struct { AliasExists bool `json:"exists"` } +// UserIDExistsRequest is a request to an application service about whether a +// user ID exists +type UserIDExistsRequest struct { + // UserID we want to lookup + UserID string `json:"user_id"` +} + +// UserIDExistsRequestAccessToken is a request to an application service +// about whether a user ID exists. Includes an access token +type UserIDExistsRequestAccessToken struct { + // UserID we want to lookup + UserID string `json:"user_id"` + AccessToken string `json:"access_token"` +} + +// UserIDExistsResponse is a response from an application service about +// whether a user ID exists +type UserIDExistsResponse struct { + UserIDExists bool `json:"exists"` +} + // AppServiceQueryAPI is used to query user and room alias data from application // services type AppServiceQueryAPI interface { @@ -45,14 +72,22 @@ type AppServiceQueryAPI interface { RoomAliasExists( ctx context.Context, req *RoomAliasExistsRequest, - response *RoomAliasExistsResponse, + resp *RoomAliasExistsResponse, + ) error + // Check whether a user ID exists within any application service namespaces + UserIDExists( + ctx context.Context, + req *UserIDExistsRequest, + resp *UserIDExistsResponse, ) error - // TODO: QueryUserIDExists } // AppServiceRoomAliasExistsPath is the HTTP path for the RoomAliasExists API const AppServiceRoomAliasExistsPath = "/api/appservice/RoomAliasExists" +// AppServiceUserIDExistsPath is the HTTP path for the UserIDExists API +const AppServiceUserIDExistsPath = "/api/appservice/UserIDExists" + // httpAppServiceQueryAPI contains the URL to an appservice query API and a // reference to a httpClient used to reach it type httpAppServiceQueryAPI struct { @@ -85,3 +120,59 @@ func (h *httpAppServiceQueryAPI) RoomAliasExists( apiURL := h.appserviceURL + AppServiceRoomAliasExistsPath return commonHTTP.PostJSON(ctx, span, h.httpClient, apiURL, request, response) } + +// UserIDExists implements AppServiceQueryAPI +func (h *httpAppServiceQueryAPI) UserIDExists( + ctx context.Context, + request *UserIDExistsRequest, + response *UserIDExistsResponse, +) error { + span, ctx := opentracing.StartSpanFromContext(ctx, "appserviceUserIDExists") + defer span.Finish() + + apiURL := h.appserviceURL + AppServiceUserIDExistsPath + return commonHTTP.PostJSON(ctx, span, h.httpClient, apiURL, request, response) +} + +// RetreiveUserProfile is a wrapper that queries both the local database and +// application services for a given user's profile +func RetreiveUserProfile( + ctx context.Context, + userID string, + asAPI AppServiceQueryAPI, + accountDB *accounts.Database, +) (*authtypes.Profile, error) { + localpart, _, err := gomatrixserverlib.SplitID('@', userID) + if err != nil { + return nil, err + } + + // Try to query the user from the local database + profile, err := accountDB.GetProfileByLocalpart(ctx, localpart) + if err != nil && err != sql.ErrNoRows { + return nil, err + } else if profile != nil { + return profile, nil + } + + // Query the appservice component for the existence of an AS user + userReq := UserIDExistsRequest{UserID: userID} + var userResp UserIDExistsResponse + if err = asAPI.UserIDExists(ctx, &userReq, &userResp); err != nil { + return nil, err + } + + // If no user exists, return + if !userResp.UserIDExists { + return nil, errors.New("no known profile for given user ID") + } + + // Try to query the user from the local database again + profile, err = accountDB.GetProfileByLocalpart(ctx, localpart) + if err != nil { + return nil, err + } + + // profile should not be nil at this point + return profile, nil +} diff --git a/src/github.com/matrix-org/dendrite/appservice/query/query.go b/src/github.com/matrix-org/dendrite/appservice/query/query.go index f15461de..fde3ab09 100644 --- a/src/github.com/matrix-org/dendrite/appservice/query/query.go +++ b/src/github.com/matrix-org/dendrite/appservice/query/query.go @@ -32,6 +32,7 @@ import ( ) const roomAliasExistsPath = "/rooms/" +const userIDExistsPath = "/users/" // AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI type AppServiceQueryAPI struct { @@ -107,6 +108,71 @@ func (a *AppServiceQueryAPI) RoomAliasExists( return nil } +// UserIDExists performs a request to '/users/{userID}' on all known +// handling application services until one admits to owning the user ID +func (a *AppServiceQueryAPI) UserIDExists( + ctx context.Context, + request *api.UserIDExistsRequest, + response *api.UserIDExistsResponse, +) error { + span, ctx := opentracing.StartSpanFromContext(ctx, "ApplicationServiceUserID") + defer span.Finish() + + // Create an HTTP client if one does not already exist + if a.HTTPClient == nil { + a.HTTPClient = makeHTTPClient() + } + + // Determine which application service should handle this request + for _, appservice := range a.Cfg.Derived.ApplicationServices { + if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) { + // The full path to the rooms API, includes hs token + URL, err := url.Parse(appservice.URL + userIDExistsPath) + URL.Path += request.UserID + apiURL := URL.String() + "?access_token=" + appservice.HSToken + + // Send a request to each application service. If one responds that it has + // created the user, immediately return. + req, err := http.NewRequest(http.MethodGet, apiURL, nil) + if err != nil { + return err + } + resp, err := a.HTTPClient.Do(req.WithContext(ctx)) + if resp != nil { + defer func() { + err = resp.Body.Close() + if err != nil { + log.WithFields(log.Fields{ + "appservice_id": appservice.ID, + "status_code": resp.StatusCode, + }).Error("Unable to close application service response body") + } + }() + } + if err != nil { + log.WithFields(log.Fields{ + "appservice_id": appservice.ID, + }).WithError(err).Error("issue querying user ID on application service") + return err + } + if resp.StatusCode == http.StatusOK { + // StatusOK received from appservice. User ID exists + response.UserIDExists = true + return nil + } + + // Log non OK + log.WithFields(log.Fields{ + "appservice_id": appservice.ID, + "status_code": resp.StatusCode, + }).Warn("application service responded with non-OK status code") + } + } + + response.UserIDExists = false + return nil +} + // makeHTTPClient creates an HTTP client with certain options that will be used for all query requests to application services func makeHTTPClient() *http.Client { return &http.Client{ @@ -131,4 +197,18 @@ func (a *AppServiceQueryAPI) SetupHTTP(servMux *http.ServeMux) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) + servMux.Handle( + api.AppServiceUserIDExistsPath, + common.MakeInternalAPI("appserviceUserIDExists", func(req *http.Request) util.JSONResponse { + var request api.UserIDExistsRequest + var response api.UserIDExistsResponse + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.ErrorResponse(err) + } + if err := a.UserIDExists(req.Context(), &request, &response); err != nil { + return util.ErrorResponse(err) + } + return util.JSONResponse{Code: http.StatusOK, JSON: &response} + }), + ) }