From be8d490e56002b45a1e97caa62435a9826f8005b Mon Sep 17 00:00:00 2001 From: Roman Isaev Date: Mon, 30 Dec 2024 17:14:04 +0000 Subject: [PATCH] mas: implemented PUT /admin/v2/users/{userID} endpoint MAS requires this endpoint to fetch the data for the account management page --- clientapi/routing/admin.go | 72 ++++++++++++++++++++++ clientapi/routing/routing.go | 13 +++- userapi/api/api.go | 11 ++-- userapi/storage/postgres/accounts_table.go | 6 +- userapi/storage/sqlite3/accounts_table.go | 5 +- 5 files changed, 95 insertions(+), 12 deletions(-) diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go index ce5476ef..0532a577 100644 --- a/clientapi/routing/admin.go +++ b/clientapi/routing/admin.go @@ -21,8 +21,10 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/exp/constraints" + appserviceAPI "github.com/element-hq/dendrite/appservice/api" clientapi "github.com/element-hq/dendrite/clientapi/api" clienthttputil "github.com/element-hq/dendrite/clientapi/httputil" + "github.com/element-hq/dendrite/clientapi/userutil" "github.com/element-hq/dendrite/internal/httputil" roomserverAPI "github.com/element-hq/dendrite/roomserver/api" "github.com/element-hq/dendrite/setup/config" @@ -731,6 +733,76 @@ func AdminCreateOrModifyAccount(req *http.Request, userAPI userapi.ClientUserAPI } } +func AdminRetrieveAccount(req *http.Request, cfg *config.ClientAPI, 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 := userutil.ParseUsernameParam(userID, cfg.Matrix) + if err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.InvalidParam(err.Error()), + } + } + + body := struct { + DisplayName string `json:"display_name"` + AvatarURL string `json:"avatar_url"` + Deactivated bool `json:"deactivated"` + }{} + + { + var rs api.QueryAccountByLocalpartResponse + err := userAPI.QueryAccountByLocalpart(req.Context(), &api.QueryAccountByLocalpartRequest{Localpart: local, ServerName: domain}, &rs) + if err == sql.ErrNoRows { + return util.JSONResponse{ + Code: http.StatusNotFound, + JSON: spec.NotFound(fmt.Sprintf("User '%s' not found", userID)), + } + } else if err != nil { + logger.WithError(err).Error("userAPI.QueryAccountByLocalpart") + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.Unknown(err.Error()), + } + } + body.Deactivated = rs.Account.Deactivated + } + + { + profile, err := userAPI.QueryProfile(req.Context(), userID) + if err != nil { + if err == appserviceAPI.ErrProfileNotExists { + return util.JSONResponse{ + Code: http.StatusNotFound, + JSON: spec.NotFound(err.Error()), + } + } else if err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.Unknown(err.Error()), + } + } + } + body.AvatarURL = profile.AvatarURL + body.DisplayName = profile.DisplayName + } + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: body, + } +} + // GetEventReports returns reported events for a given user/room. func GetEventReports( req *http.Request, diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 945d0e48..142b1f81 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -344,9 +344,16 @@ func Setup( })).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) + httputil.MakeServiceAdminAPI("admin_manage_user", m.AdminToken, func(r *http.Request) util.JSONResponse { + switch r.Method { + case http.MethodGet: + return AdminRetrieveAccount(r, cfg, userAPI) + case http.MethodPut: + return AdminCreateOrModifyAccount(r, userAPI) + default: + return util.JSONResponse{Code: http.StatusMethodNotAllowed, JSON: nil} + } + })).Methods(http.MethodPut, http.MethodGet) synapseAdminRouter.Handle("/admin/v2/users/{userID}/devices", httputil.MakeServiceAdminAPI("admin_user_devices", m.AdminToken, func(r *http.Request) util.JSONResponse { diff --git a/userapi/api/api.go b/userapi/api/api.go index bcd5c9c0..5387276b 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -31,7 +31,6 @@ type UserInternalAPI interface { FederationUserAPI 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) } @@ -89,6 +88,7 @@ type ClientUserAPI interface { QueryPushers(ctx context.Context, req *QueryPushersRequest, res *QueryPushersResponse) error QueryPushRules(ctx context.Context, userID string) (*pushrules.AccountRuleSets, error) QueryAccountAvailability(ctx context.Context, req *QueryAccountAvailabilityRequest, res *QueryAccountAvailabilityResponse) error + QueryAccountByLocalpart(ctx context.Context, req *QueryAccountByLocalpartRequest, res *QueryAccountByLocalpartResponse) (err error) PerformAdminCreateRegistrationToken(ctx context.Context, registrationToken *clientapi.RegistrationToken) (bool, error) PerformAdminListRegistrationTokens(ctx context.Context, returnAll bool, valid bool) ([]clientapi.RegistrationToken, error) PerformAdminGetRegistrationToken(ctx context.Context, tokenString string) (*clientapi.RegistrationToken, error) @@ -461,6 +461,7 @@ type Account struct { ServerName spec.ServerName AppServiceID string AccountType AccountType + Deactivated bool // TODO: Associations (e.g. with application services) } @@ -660,7 +661,7 @@ type QueryAccountByLocalpartResponse struct { } type QueryLocalpartExternalIDRequest struct { - ExternalID string + ExternalID string AuthProvider string } @@ -669,8 +670,8 @@ type QueryLocalpartExternalIDResponse struct { } type PerformLocalpartExternalUserIDCreationRequest struct { - Localpart string - ExternalID string + Localpart string + ExternalID string AuthProvider string } @@ -914,7 +915,7 @@ type PerformUploadDeviceKeysResponse struct { } type PerformAllowingMasterCrossSigningKeyReplacementWithoutUIARequest struct { - UserID string + UserID string Duration time.Duration } diff --git a/userapi/storage/postgres/accounts_table.go b/userapi/storage/postgres/accounts_table.go index 489017fb..5c051996 100644 --- a/userapi/storage/postgres/accounts_table.go +++ b/userapi/storage/postgres/accounts_table.go @@ -55,7 +55,7 @@ const deactivateAccountSQL = "" + "UPDATE userapi_accounts SET is_deactivated = TRUE WHERE localpart = $1 AND server_name = $2" const selectAccountByLocalpartSQL = "" + - "SELECT localpart, server_name, appservice_id, account_type FROM userapi_accounts WHERE localpart = $1 AND server_name = $2" + "SELECT localpart, server_name, appservice_id, account_type, is_deactivated FROM userapi_accounts WHERE localpart = $1 AND server_name = $2" const selectPasswordHashSQL = "" + "SELECT password_hash FROM userapi_accounts WHERE localpart = $1 AND server_name = $2 AND is_deactivated = FALSE" @@ -116,6 +116,7 @@ func (s *accountsStatements) InsertAccount( localpart string, serverName spec.ServerName, hash, appserviceID string, accountType api.AccountType, ) (*api.Account, error) { + // TODO: can we replace "UnixNano() / 1M" with "UnixMilli()"? createdTimeMS := time.Now().UnixNano() / 1000000 stmt := sqlutil.TxStmt(txn, s.insertAccountStmt) @@ -135,6 +136,7 @@ func (s *accountsStatements) InsertAccount( ServerName: serverName, AppServiceID: appserviceID, AccountType: accountType, + Deactivated: false, }, nil } @@ -167,7 +169,7 @@ func (s *accountsStatements) SelectAccountByLocalpart( var acc api.Account stmt := s.selectAccountByLocalpartStmt - err := stmt.QueryRowContext(ctx, localpart, serverName).Scan(&acc.Localpart, &acc.ServerName, &appserviceIDPtr, &acc.AccountType) + err := stmt.QueryRowContext(ctx, localpart, serverName).Scan(&acc.Localpart, &acc.ServerName, &appserviceIDPtr, &acc.AccountType, &acc.Deactivated) if err != nil { if err != sql.ErrNoRows { log.WithError(err).Error("Unable to retrieve user from the db") diff --git a/userapi/storage/sqlite3/accounts_table.go b/userapi/storage/sqlite3/accounts_table.go index 66cc7c06..7c627919 100644 --- a/userapi/storage/sqlite3/accounts_table.go +++ b/userapi/storage/sqlite3/accounts_table.go @@ -54,7 +54,7 @@ const deactivateAccountSQL = "" + "UPDATE userapi_accounts SET is_deactivated = 1 WHERE localpart = $1 AND server_name = $2" const selectAccountByLocalpartSQL = "" + - "SELECT localpart, server_name, appservice_id, account_type FROM userapi_accounts WHERE localpart = $1 AND server_name = $2" + "SELECT localpart, server_name, appservice_id, account_type, is_deactivated FROM userapi_accounts WHERE localpart = $1 AND server_name = $2" const selectPasswordHashSQL = "" + "SELECT password_hash FROM userapi_accounts WHERE localpart = $1 AND server_name = $2 AND is_deactivated = 0" @@ -135,6 +135,7 @@ func (s *accountsStatements) InsertAccount( ServerName: serverName, AppServiceID: appserviceID, AccountType: accountType, + Deactivated: false, }, nil } @@ -167,7 +168,7 @@ func (s *accountsStatements) SelectAccountByLocalpart( var acc api.Account stmt := s.selectAccountByLocalpartStmt - err := stmt.QueryRowContext(ctx, localpart, serverName).Scan(&acc.Localpart, &acc.ServerName, &appserviceIDPtr, &acc.AccountType) + err := stmt.QueryRowContext(ctx, localpart, serverName).Scan(&acc.Localpart, &acc.ServerName, &appserviceIDPtr, &acc.AccountType, &acc.Deactivated) if err != nil { if err != sql.ErrNoRows { log.WithError(err).Error("Unable to retrieve user from the db")