From 6a4e3293f84dce5cfddbbc245139ca74b7f05a3f Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Fri, 17 Jul 2020 10:00:43 +0300 Subject: [PATCH] [MM-25648] api4: add user/bot convert endpoints (#14877) * api4: add user/bot convert endpoints * api4: add convert user/bot to local mode * api4: fix linting issues * api4/bot: reflect review comments * api4: update convert user endpoint paths * remove shadow decl * fix translation problems --- api4/bot.go | 44 ++++++++++++++++++++ api4/bot_local.go | 2 + api4/bot_test.go | 62 ++++++++++++++++++++++++++++ api4/user.go | 34 +++++++++++++++ api4/user_local.go | 1 + api4/user_test.go | 26 ++++++++++++ app/app_iface.go | 2 + app/opentracing/opentracing_layer.go | 22 ++++++++++ app/user.go | 37 +++++++++++++++++ i18n/en.json | 4 ++ model/client4.go | 24 +++++++++++ 11 files changed, 258 insertions(+) diff --git a/api4/bot.go b/api4/bot.go index af5d2208f6..def2d13ba6 100644 --- a/api4/bot.go +++ b/api4/bot.go @@ -21,6 +21,7 @@ func (api *API) InitBot() { api.BaseRoutes.Bots.Handle("", api.ApiSessionRequired(getBots)).Methods("GET") api.BaseRoutes.Bot.Handle("/disable", api.ApiSessionRequired(disableBot)).Methods("POST") api.BaseRoutes.Bot.Handle("/enable", api.ApiSessionRequired(enableBot)).Methods("POST") + api.BaseRoutes.Bot.Handle("/convert_to_user", api.ApiSessionRequired(convertBotToUser)).Methods("POST") api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.ApiSessionRequired(assignBot)).Methods("POST") api.BaseRoutes.Bot.Handle("/icon", api.ApiSessionRequiredTrustRequester(getBotIconImage)).Methods("GET") @@ -378,3 +379,46 @@ func deleteBotIconImage(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } + +func convertBotToUser(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireBotUserId() + if c.Err != nil { + return + } + + bot, err := c.App.GetBot(c.Params.BotUserId, false) + if err != nil { + c.Err = err + return + } + + userPatch := model.UserPatchFromJson(r.Body) + if userPatch == nil || userPatch.Password == nil || *userPatch.Password == "" { + c.SetInvalidParam("userPatch") + return + } + + systemAdmin, _ := strconv.ParseBool(r.URL.Query().Get("set_system_admin")) + + auditRec := c.MakeAuditRecord("convertBotToUser", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("bot", bot) + auditRec.AddMeta("userPatch", userPatch) + auditRec.AddMeta("set_system_admin", systemAdmin) + + if c.Params.UserId != c.App.Session().UserId && !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + user, err := c.App.ConvertBotToUser(bot, userPatch, systemAdmin) + if err != nil { + c.Err = err + return + } + + auditRec.Success() + auditRec.AddMeta("convertedTo", user) + + w.Write([]byte(user.ToJson())) +} diff --git a/api4/bot_local.go b/api4/bot_local.go index 6d2662effc..369ba62a75 100644 --- a/api4/bot_local.go +++ b/api4/bot_local.go @@ -4,9 +4,11 @@ package api4 func (api *API) InitBotLocal() { + api.BaseRoutes.Bot.Handle("", api.ApiLocal(getBot)).Methods("GET") api.BaseRoutes.Bot.Handle("", api.ApiLocal(patchBot)).Methods("PUT") api.BaseRoutes.Bot.Handle("/disable", api.ApiLocal(disableBot)).Methods("POST") api.BaseRoutes.Bot.Handle("/enable", api.ApiLocal(enableBot)).Methods("POST") + api.BaseRoutes.Bot.Handle("/convert_to_user", api.ApiLocal(convertBotToUser)).Methods("POST") api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.ApiLocal(assignBot)).Methods("POST") api.BaseRoutes.Bots.Handle("", api.ApiLocal(getBots)).Methods("GET") diff --git a/api4/bot_test.go b/api4/bot_test.go index 1e7073b050..19b6b501e0 100644 --- a/api4/bot_test.go +++ b/api4/bot_test.go @@ -1403,6 +1403,68 @@ func TestDeleteBotIconImage(t *testing.T) { require.False(t, exists, "icon.svg should not for the user") } +func TestConvertBotToUser(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.EnableBotAccountCreation = true + }) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + bot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot.UserId) + + _, resp = th.Client.ConvertBotToUser(bot.UserId, &model.UserPatch{}, false) + CheckBadRequestStatus(t, resp) + + user, resp := th.Client.ConvertBotToUser(bot.UserId, &model.UserPatch{Password: model.NewString("password")}, false) + CheckForbiddenStatus(t, resp) + require.Nil(t, user) + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + bot, resp := th.SystemAdminClient.CreateBot(bot) + CheckCreatedStatus(t, resp) + + user, resp := client.ConvertBotToUser(bot.UserId, &model.UserPatch{}, false) + CheckBadRequestStatus(t, resp) + + user, resp = client.ConvertBotToUser(bot.UserId, &model.UserPatch{Password: model.NewString("password")}, false) + CheckNoError(t, resp) + require.NotNil(t, user) + require.Equal(t, bot.UserId, user.Id) + + bot, resp = client.GetBot(bot.UserId, "") + CheckNotFoundStatus(t, resp) + + bot = &model.Bot{ + Username: GenerateTestUsername(), + Description: "systemAdminBot", + } + bot, resp = th.SystemAdminClient.CreateBot(bot) + CheckCreatedStatus(t, resp) + + user, resp = client.ConvertBotToUser(bot.UserId, &model.UserPatch{Password: model.NewString("password")}, true) + CheckNoError(t, resp) + require.NotNil(t, user) + require.Equal(t, bot.UserId, user.Id) + require.Contains(t, user.GetRoles(), model.SYSTEM_ADMIN_ROLE_ID) + + bot, resp = client.GetBot(bot.UserId, "") + CheckNotFoundStatus(t, resp) + }) +} + func sToP(s string) *string { return &s } diff --git a/api4/user.go b/api4/user.go index 78099497a5..2a1bc67a03 100644 --- a/api4/user.go +++ b/api4/user.go @@ -46,6 +46,7 @@ func (api *API) InitUser() { api.BaseRoutes.User.Handle("/password", api.ApiSessionRequired(updatePassword)).Methods("PUT") api.BaseRoutes.User.Handle("/promote", api.ApiSessionRequired(promoteGuestToUser)).Methods("POST") api.BaseRoutes.User.Handle("/demote", api.ApiSessionRequired(demoteUserToGuest)).Methods("POST") + api.BaseRoutes.User.Handle("/convert_to_bot", api.ApiSessionRequired(convertUserToBot)).Methods("POST") api.BaseRoutes.Users.Handle("/password/reset", api.ApiHandler(resetPassword)).Methods("POST") api.BaseRoutes.Users.Handle("/password/reset/send", api.ApiHandler(sendPasswordReset)).Methods("POST") api.BaseRoutes.Users.Handle("/email/verify", api.ApiHandler(verifyUserEmail)).Methods("POST") @@ -2485,3 +2486,36 @@ func verifyUserEmailWithoutToken(c *Context, w http.ResponseWriter, r *http.Requ w.Write([]byte(user.ToJson())) } + +func convertUserToBot(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + user, err := c.App.GetUser(c.Params.UserId) + if err != nil { + c.Err = err + return + } + + auditRec := c.MakeAuditRecord("convertUserToBot", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("user", user) + + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + bot, err := c.App.ConvertUserToBot(user) + if err != nil { + c.Err = err + return + } + + auditRec.Success() + auditRec.AddMeta("convertedTo", bot) + + w.Write(bot.ToJson()) +} diff --git a/api4/user_local.go b/api4/user_local.go index 68b36b66b4..2f54209f33 100644 --- a/api4/user_local.go +++ b/api4/user_local.go @@ -22,6 +22,7 @@ func (api *API) InitUserLocal() { api.BaseRoutes.User.Handle("/roles", api.ApiLocal(updateUserRoles)).Methods("PUT") api.BaseRoutes.User.Handle("/mfa", api.ApiLocal(updateUserMfa)).Methods("PUT") api.BaseRoutes.User.Handle("/active", api.ApiLocal(updateUserActive)).Methods("PUT") + api.BaseRoutes.User.Handle("/convert_to_bot", api.ApiLocal(convertUserToBot)).Methods("POST") api.BaseRoutes.UserByUsername.Handle("", api.ApiLocal(localGetUserByUsername)).Methods("GET") api.BaseRoutes.UserByEmail.Handle("", api.ApiLocal(localGetUserByEmail)).Methods("GET") diff --git a/api4/user_test.go b/api4/user_test.go index 84262bc581..e988a42a81 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -5075,3 +5075,29 @@ func TestPublishUserTyping(t *testing.T) { CheckServiceUnavailableStatus(t, resp) }) } + +func TestConvertUserToBot(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, resp := th.Client.ConvertUserToBot(th.BasicUser.Id) + CheckForbiddenStatus(t, resp) + require.Nil(t, bot) + + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + user := model.User{Email: th.GenerateTestEmail(), Username: GenerateTestUsername(), Password: "password"} + + ruser, resp := client.CreateUser(&user) + CheckNoError(t, resp) + CheckCreatedStatus(t, resp) + + bot, resp = client.ConvertUserToBot(ruser.Id) + CheckNoError(t, resp) + require.NotNil(t, bot) + require.Equal(t, bot.UserId, ruser.Id) + + bot, resp = client.GetBot(bot.UserId, "") + CheckNoError(t, resp) + require.NotNil(t, bot) + }) +} diff --git a/app/app_iface.go b/app/app_iface.go index 0b80900905..33381a9823 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -61,6 +61,8 @@ type AppIface interface { ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) // ClientConfigWithComputed gets the configuration in a format suitable for sending to the client. ClientConfigWithComputed() map[string]string + // ConvertBotToUser converts a bot to user. + ConvertBotToUser(bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) // ConvertUserToBot converts a user to bot. ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) // CreateBot creates the given bot and corresponding user. diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 7033526f3d..a7c96fc6a6 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -1362,6 +1362,28 @@ func (a *OpenTracingAppLayer) Config() *model.Config { return resultVar0 } +func (a *OpenTracingAppLayer) ConvertBotToUser(bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertBotToUser") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.ConvertBotToUser(bot, userPatch, sysadmin) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertUserToBot") diff --git a/app/user.go b/app/user.go index 41b1d2d197..5435d042a6 100644 --- a/app/user.go +++ b/app/user.go @@ -2140,3 +2140,40 @@ func (a *App) invalidateUserCacheAndPublish(userId string) { func (a *App) GetKnownUsers(userID string) ([]string, *model.AppError) { return a.Srv().Store.User().GetKnownUsers(userID) } + +// ConvertBotToUser converts a bot to user. +func (a *App) ConvertBotToUser(bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError) { + user, err := a.Srv().Store.User().Get(bot.UserId) + if err != nil { + return nil, err + } + + if sysadmin && !user.IsInRole(model.SYSTEM_ADMIN_ROLE_ID) { + _, err = a.UpdateUserRoles( + user.Id, + fmt.Sprintf("%s %s", user.Roles, model.SYSTEM_ADMIN_ROLE_ID), + false) + if err != nil { + return nil, err + } + } + + user.Patch(userPatch) + + user, err = a.UpdateUser(user, false) + if err != nil { + return nil, err + } + + err = a.UpdatePassword(user, *userPatch.Password) + if err != nil { + return nil, err + } + + appErr := a.Srv().Store.Bot().PermanentDelete(bot.UserId) + if appErr != nil { + return nil, model.NewAppError("ConvertBotToUser", "", nil, err.Error(), http.StatusInternalServerError) + } + + return user, nil +} diff --git a/i18n/en.json b/i18n/en.json index 0a2c21a883..a5400dd97c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,4 +1,8 @@ [ + { + "id": "", + "translation": "" + }, { "id": "April", "translation": "April" diff --git a/model/client4.go b/model/client4.go index fe15fecbe6..312096ab2d 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1226,6 +1226,30 @@ func (c *Client4) DeleteUser(userId string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } +// ConvertUserToBot converts a user to a bot user. +func (c *Client4) ConvertUserToBot(userId string) (*Bot, *Response) { + r, err := c.DoApiPost(c.GetUserRoute(userId)+"/convert_to_bot", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// ConvertBotToUser converts a bot user to a user. +func (c *Client4) ConvertBotToUser(userId string, userPatch *UserPatch, setSystemAdmin bool) (*User, *Response) { + var query string + if setSystemAdmin { + query = "?set_system_admin=true" + } + r, err := c.DoApiPost(c.GetBotRoute(userId)+"/convert_to_user"+query, userPatch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + // PermanentDeleteAll permanently deletes all users in the system. This is a local only endpoint func (c *Client4) PermanentDeleteAllUsers() (bool, *Response) { r, err := c.DoApiDelete(c.GetUsersRoute())