diff --git a/Gopkg.lock b/Gopkg.lock index 67ec258866..e46c3a9f7e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -443,12 +443,12 @@ revision = "60711f1a8329503b04e1c88535f419d0bb440bff" [[projects]] - branch = "master" - digest = "1:f44dea49cf8d9389516c537b7ef61bfb5836a9bf485e213673917258413f24c3" + branch = "mm-14140" + digest = "1:697ef923111e6b1a9fc09d4e799cb071469dbaccb371eb707832407cbde07413" name = "github.com/mattermost/gorp" packages = ["."] pruneopts = "UT" - revision = "520a119fe3536337cf8feeaf76afae0f5ae193f1" + revision = "a13faa4e058457a4e0e0c02268cd6561b55c9702" [[projects]] branch = "master" diff --git a/api4/api.go b/api4/api.go index b062d6141b..f943a73a7e 100644 --- a/api4/api.go +++ b/api4/api.go @@ -24,6 +24,9 @@ type Routes struct { UserByUsername *mux.Router // 'api/v4/users/username/{username:[A-Za-z0-9_-\.]+}' UserByEmail *mux.Router // 'api/v4/users/email/{email}' + Bots *mux.Router // 'api/v4/bots' + Bot *mux.Router // 'api/v4/bots/{bot_user_id:[A-Za-z0-9]+}' + Teams *mux.Router // 'api/v4/teams' TeamsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams' Team *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}' @@ -132,6 +135,9 @@ func Init(configservice configservice.ConfigService, globalOptionsFunc app.AppOp api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email}").Subrouter() + api.BaseRoutes.Bots = api.BaseRoutes.ApiRoot.PathPrefix("/bots").Subrouter() + api.BaseRoutes.Bot = api.BaseRoutes.ApiRoot.PathPrefix("/bots/{bot_user_id:[A-Za-z0-9]+}").Subrouter() + api.BaseRoutes.Teams = api.BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter() api.BaseRoutes.TeamsForUser = api.BaseRoutes.User.PathPrefix("/teams").Subrouter() api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter() @@ -209,6 +215,7 @@ func Init(configservice configservice.ConfigService, globalOptionsFunc app.AppOp api.BaseRoutes.Groups = api.BaseRoutes.ApiRoot.PathPrefix("/groups").Subrouter() api.InitUser() + api.InitBot() api.InitTeam() api.InitChannel() api.InitPost() diff --git a/api4/bot.go b/api4/bot.go new file mode 100644 index 0000000000..fedb99361c --- /dev/null +++ b/api4/bot.go @@ -0,0 +1,200 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" + + "github.com/mattermost/mattermost-server/model" +) + +func (api *API) InitBot() { + api.BaseRoutes.Bots.Handle("", api.ApiSessionRequired(createBot)).Methods("POST") + api.BaseRoutes.Bot.Handle("", api.ApiSessionRequired(patchBot)).Methods("PUT") + api.BaseRoutes.Bot.Handle("", api.ApiSessionRequired(getBot)).Methods("GET") + 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("/assign/{user_id:[A-Za-z0-9]+}", api.ApiSessionRequired(assignBot)).Methods("POST") +} + +func createBot(c *Context, w http.ResponseWriter, r *http.Request) { + botPatch := model.BotPatchFromJson(r.Body) + if botPatch == nil { + c.SetInvalidParam("bot") + return + } + + bot := &model.Bot{ + OwnerId: c.App.Session.UserId, + } + bot.Patch(botPatch) + + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_CREATE_BOT) { + c.SetPermissionError(model.PERMISSION_CREATE_BOT) + return + } + + createdBot, err := c.App.CreateBot(bot) + if err != nil { + c.Err = err + return + } + + w.WriteHeader(http.StatusCreated) + w.Write(createdBot.ToJson()) +} + +func patchBot(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireBotUserId() + if c.Err != nil { + return + } + botUserId := c.Params.BotUserId + + botPatch := model.BotPatchFromJson(r.Body) + if botPatch == nil { + c.SetInvalidParam("bot") + return + } + + if err := c.App.SessionHasPermissionToManageBot(c.App.Session, botUserId); err != nil { + c.Err = err + return + } + + updatedBot, err := c.App.PatchBot(botUserId, botPatch) + if err != nil { + c.Err = err + return + } + + w.Write(updatedBot.ToJson()) +} + +func getBot(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireBotUserId() + if c.Err != nil { + return + } + botUserId := c.Params.BotUserId + + includeDeleted := r.URL.Query().Get("include_deleted") == "true" + + bot, err := c.App.GetBot(botUserId, includeDeleted) + if err != nil { + c.Err = err + return + } + + if c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_READ_OTHERS_BOTS) { + // Allow access to any bot. + } else if bot.OwnerId == c.App.Session.UserId { + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_READ_BOTS) { + // Pretend like the bot doesn't exist at all to avoid revealing that the + // user is a bot. It's kind of silly in this case, sine we created the bot, + // but we don't have read bot permissions. + c.Err = model.MakeBotNotFoundError(botUserId) + return + } + } else { + // Pretend like the bot doesn't exist at all, to avoid revealing that the + // user is a bot. + c.Err = model.MakeBotNotFoundError(botUserId) + return + } + + if c.HandleEtag(bot.Etag(), "Get Bot", w, r) { + return + } + + w.Write(bot.ToJson()) +} + +func getBots(c *Context, w http.ResponseWriter, r *http.Request) { + includeDeleted := r.URL.Query().Get("include_deleted") == "true" + onlyOrphaned := r.URL.Query().Get("only_orphaned") == "true" + + var OwnerId string + if c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_READ_OTHERS_BOTS) { + // Get bots created by any user. + OwnerId = "" + } else if c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_READ_BOTS) { + // Only get bots created by this user. + OwnerId = c.App.Session.UserId + } else { + c.SetPermissionError(model.PERMISSION_READ_BOTS) + return + } + + bots, err := c.App.GetBots(&model.BotGetOptions{ + Page: c.Params.Page, + PerPage: c.Params.PerPage, + OwnerId: OwnerId, + IncludeDeleted: includeDeleted, + OnlyOrphaned: onlyOrphaned, + }) + if err != nil { + c.Err = err + return + } + + if c.HandleEtag(bots.Etag(), "Get Bots", w, r) { + return + } + + w.Write(bots.ToJson()) +} + +func disableBot(c *Context, w http.ResponseWriter, r *http.Request) { + updateBotActive(c, w, r, false) +} + +func enableBot(c *Context, w http.ResponseWriter, r *http.Request) { + updateBotActive(c, w, r, true) +} + +func updateBotActive(c *Context, w http.ResponseWriter, r *http.Request, active bool) { + c.RequireBotUserId() + if c.Err != nil { + return + } + botUserId := c.Params.BotUserId + + if err := c.App.SessionHasPermissionToManageBot(c.App.Session, botUserId); err != nil { + c.Err = err + return + } + + bot, err := c.App.UpdateBotActive(botUserId, active) + if err != nil { + c.Err = err + return + } + + w.Write(bot.ToJson()) +} + +func assignBot(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + c.RequireBotUserId() + if c.Err != nil { + return + } + botUserId := c.Params.BotUserId + userId := c.Params.UserId + + if err := c.App.SessionHasPermissionToManageBot(c.App.Session, botUserId); err != nil { + c.Err = err + return + } + + bot, err := c.App.UpdateBotOwner(botUserId, userId) + if err != nil { + c.Err = err + return + } + + w.Write(bot.ToJson()) +} diff --git a/api4/bot_test.go b/api4/bot_test.go new file mode 100644 index 0000000000..e22045387c --- /dev/null +++ b/api4/bot_test.go @@ -0,0 +1,938 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "io/ioutil" + "strings" + "testing" + + "github.com/mattermost/mattermost-server/model" + "github.com/stretchr/testify/require" +) + +func TestCreateBot(t *testing.T) { + t.Run("create bot without permissions", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + _, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + + CheckErrorMessage(t, resp, "api.context.permissions.app_error") + }) + + t.Run("create bot with permissions", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + } + + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + require.Equal(t, bot.Username, createdBot.Username) + require.Equal(t, bot.DisplayName, createdBot.DisplayName) + require.Equal(t, bot.Description, createdBot.Description) + require.Equal(t, th.BasicUser.Id, createdBot.OwnerId) + }) + + t.Run("create invalid bot", func(t *testing.T) { + th := Setup().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) + + _, resp := th.Client.CreateBot(&model.Bot{ + Username: "username", + DisplayName: "a bot", + Description: strings.Repeat("x", 1025), + }) + + CheckErrorMessage(t, resp, "model.bot.is_valid.description.app_error") + }) +} + +func TestPatchBot(t *testing.T) { + t.Run("patch non-existent bot", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + _, resp := th.SystemAdminClient.PatchBot(model.NewId(), &model.BotPatch{}) + CheckNotFoundStatus(t, resp) + }) + + t.Run("patch someone else's bot without permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + _, resp = th.Client.PatchBot(createdBot.UserId, &model.BotPatch{}) + CheckErrorMessage(t, resp, "store.sql_bot.get.missing.app_error") + }) + + t.Run("patch someone else's bot without permission, but with read others permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + _, resp = th.Client.PatchBot(createdBot.UserId, &model.BotPatch{}) + CheckErrorMessage(t, resp, "api.context.permissions.app_error") + }) + + t.Run("patch someone else's bot with permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP(GenerateTestUsername()), + DisplayName: sToP("an updated bot"), + Description: sToP("updated bot"), + } + + patchedBot, resp := th.Client.PatchBot(createdBot.UserId, botPatch) + CheckOKStatus(t, resp) + require.Equal(t, *botPatch.Username, patchedBot.Username) + require.Equal(t, *botPatch.DisplayName, patchedBot.DisplayName) + require.Equal(t, *botPatch.Description, patchedBot.Description) + require.Equal(t, th.SystemAdminUser.Id, patchedBot.OwnerId) + }) + + t.Run("patch my bot without permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP(GenerateTestUsername()), + DisplayName: sToP("an updated bot"), + Description: sToP("updated bot"), + } + + _, resp = th.Client.PatchBot(createdBot.UserId, botPatch) + CheckErrorMessage(t, resp, "store.sql_bot.get.missing.app_error") + }) + + t.Run("patch my bot without permission, but with read permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP(GenerateTestUsername()), + DisplayName: sToP("an updated bot"), + Description: sToP("updated bot"), + } + + _, resp = th.Client.PatchBot(createdBot.UserId, botPatch) + CheckErrorMessage(t, resp, "api.context.permissions.app_error") + }) + + t.Run("patch my bot with permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP(GenerateTestUsername()), + DisplayName: sToP("an updated bot"), + Description: sToP("updated bot"), + } + + patchedBot, resp := th.Client.PatchBot(createdBot.UserId, botPatch) + CheckOKStatus(t, resp) + require.Equal(t, *botPatch.Username, patchedBot.Username) + require.Equal(t, *botPatch.DisplayName, patchedBot.DisplayName) + require.Equal(t, *botPatch.Description, patchedBot.Description) + require.Equal(t, th.BasicUser.Id, patchedBot.OwnerId) + }) + + t.Run("partial patch my bot with permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + } + + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP(GenerateTestUsername()), + } + + patchedBot, resp := th.Client.PatchBot(createdBot.UserId, botPatch) + CheckOKStatus(t, resp) + require.Equal(t, *botPatch.Username, patchedBot.Username) + require.Equal(t, bot.DisplayName, patchedBot.DisplayName) + require.Equal(t, bot.Description, patchedBot.Description) + require.Equal(t, th.BasicUser.Id, patchedBot.OwnerId) + }) + + t.Run("update bot, internally managed fields ignored", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + r, err := th.Client.DoApiPut(th.Client.GetBotRoute(createdBot.UserId), `{"creator_id":"`+th.BasicUser2.Id+`"}`) + require.Nil(t, err) + defer func() { + _, _ = ioutil.ReadAll(r.Body) + _ = r.Body.Close() + }() + patchedBot := model.BotFromJson(r.Body) + resp = model.BuildResponse(r) + CheckOKStatus(t, resp) + + require.Equal(t, th.BasicUser.Id, patchedBot.OwnerId) + }) +} + +func TestGetBot(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + bot1, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "the first bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot1.UserId) + + bot2, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "another bot", + Description: "the second bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot2.UserId) + + deletedBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + Description: "a deleted bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(deletedBot.UserId) + deletedBot, resp = th.SystemAdminClient.DisableBot(deletedBot.UserId) + CheckOKStatus(t, resp) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + myBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "my bot", + Description: "a bot created by non-admin", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(myBot.UserId) + th.RemovePermissionFromRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + + t.Run("get unknown bot", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + _, resp := th.Client.GetBot(model.NewId(), "") + CheckNotFoundStatus(t, resp) + }) + + t.Run("get bot1", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot, resp := th.Client.GetBot(bot1.UserId, "") + CheckOKStatus(t, resp) + require.Equal(t, bot1, bot) + + bot, resp = th.Client.GetBot(bot1.UserId, bot.Etag()) + CheckEtag(t, bot, resp) + }) + + t.Run("get bot2", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot, resp := th.Client.GetBot(bot2.UserId, "") + CheckOKStatus(t, resp) + require.Equal(t, bot2, bot) + + bot, resp = th.Client.GetBot(bot2.UserId, bot.Etag()) + CheckEtag(t, bot, resp) + }) + + t.Run("get bot1 without READ_OTHERS_BOTS permission", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + _, resp := th.Client.GetBot(bot1.UserId, "") + CheckErrorMessage(t, resp, "store.sql_bot.get.missing.app_error") + }) + + t.Run("get myBot without READ_BOTS OR READ_OTHERS_BOTS permissions", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + _, resp := th.Client.GetBot(myBot.UserId, "") + CheckErrorMessage(t, resp, "store.sql_bot.get.missing.app_error") + }) + + t.Run("get deleted bot", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + _, resp := th.Client.GetBot(deletedBot.UserId, "") + CheckNotFoundStatus(t, resp) + }) + + t.Run("get deleted bot, include deleted", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot, resp := th.Client.GetBotIncludeDeleted(deletedBot.UserId, "") + CheckOKStatus(t, resp) + require.NotEqual(t, 0, bot.DeleteAt) + deletedBot.UpdateAt = bot.UpdateAt + deletedBot.DeleteAt = bot.DeleteAt + require.Equal(t, deletedBot, bot) + + bot, resp = th.Client.GetBotIncludeDeleted(deletedBot.UserId, bot.Etag()) + CheckEtag(t, bot, resp) + }) +} + +func TestGetBots(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + bot1, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "the first bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot1.UserId) + + deletedBot1, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + Description: "a deleted bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(deletedBot1.UserId) + deletedBot1, resp = th.SystemAdminClient.DisableBot(deletedBot1.UserId) + CheckOKStatus(t, resp) + + bot2, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "another bot", + Description: "the second bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot2.UserId) + + bot3, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "another bot", + Description: "the third bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot3.UserId) + + deletedBot2, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + Description: "a deleted bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(deletedBot2.UserId) + deletedBot2, resp = th.SystemAdminClient.DisableBot(deletedBot2.UserId) + CheckOKStatus(t, resp) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser2.Id, model.TEAM_USER_ROLE_ID, false) + th.LoginBasic2() + orphanedBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + Description: "an oprphaned bot", + }) + CheckCreatedStatus(t, resp) + th.LoginBasic() + defer th.App.PermanentDeleteBot(orphanedBot.UserId) + // Automatic deactivation disabled + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated = false + }) + _, resp = th.SystemAdminClient.DeleteUser(th.BasicUser2.Id) + CheckOKStatus(t, resp) + + t.Run("get bots, page=0, perPage=10", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBots(0, 10, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{bot1, bot2, bot3, orphanedBot}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBots(0, 10, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=0, perPage=1", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBots(0, 1, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{bot1}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBots(0, 1, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=1, perPage=2", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBots(1, 2, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{bot3, orphanedBot}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBots(1, 2, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=2, perPage=2", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBots(2, 2, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBots(2, 2, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=0, perPage=10, include deleted", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBotsIncludeDeleted(0, 10, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{bot1, deletedBot1, bot2, bot3, deletedBot2, orphanedBot}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBotsIncludeDeleted(0, 10, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=0, perPage=1, include deleted", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBotsIncludeDeleted(0, 1, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{bot1}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBotsIncludeDeleted(0, 1, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=1, perPage=2, include deleted", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBotsIncludeDeleted(1, 2, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{bot2, bot3}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBotsIncludeDeleted(1, 2, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=2, perPage=2, include deleted", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBotsIncludeDeleted(2, 2, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{deletedBot2, orphanedBot}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBotsIncludeDeleted(2, 2, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots, page=0, perPage=10, only orphaned", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bots, resp := th.Client.GetBotsOrphaned(0, 10, "") + CheckOKStatus(t, resp) + require.Equal(t, []*model.Bot{orphanedBot}, bots) + + botList := model.BotList(bots) + bots, resp = th.Client.GetBotsOrphaned(0, 10, botList.Etag()) + CheckEtag(t, bots, resp) + }) + + t.Run("get bots without permission", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + _, resp := th.Client.GetBots(0, 10, "") + CheckErrorMessage(t, resp, "api.context.permissions.app_error") + }) +} + +func TestDisableBot(t *testing.T) { + t.Run("disable non-existent bot", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + _, resp := th.Client.DisableBot(model.NewId()) + CheckNotFoundStatus(t, resp) + }) + + t.Run("disable bot without permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + _, resp = th.Client.DisableBot(createdBot.UserId) + CheckErrorMessage(t, resp, "store.sql_bot.get.missing.app_error") + }) + + t.Run("disable bot without permission, but with read permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + _, resp = th.Client.DisableBot(createdBot.UserId) + CheckErrorMessage(t, resp, "api.context.permissions.app_error") + }) + + t.Run("disable bot with permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot.UserId) + + enabledBot1, resp := th.Client.DisableBot(bot.UserId) + CheckOKStatus(t, resp) + bot.UpdateAt = enabledBot1.UpdateAt + bot.DeleteAt = enabledBot1.DeleteAt + require.Equal(t, bot, enabledBot1) + + // Check bot disabled + disab, resp := th.SystemAdminClient.GetBotIncludeDeleted(bot.UserId, "") + CheckOKStatus(t, resp) + require.NotZero(t, disab.DeleteAt) + + // Disabling should be idempotent. + enabledBot2, resp := th.Client.DisableBot(bot.UserId) + CheckOKStatus(t, resp) + require.Equal(t, bot, enabledBot2) + }) +} + +func TestEnableBot(t *testing.T) { + t.Run("enable non-existent bot", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + _, resp := th.Client.EnableBot(model.NewId()) + CheckNotFoundStatus(t, resp) + }) + + t.Run("enable bot without permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + _, resp = th.SystemAdminClient.DisableBot(createdBot.UserId) + CheckOKStatus(t, resp) + + _, resp = th.Client.EnableBot(createdBot.UserId) + CheckErrorMessage(t, resp, "store.sql_bot.get.missing.app_error") + }) + + t.Run("enable bot without permission, but with read permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + _, resp = th.SystemAdminClient.DisableBot(createdBot.UserId) + CheckOKStatus(t, resp) + + _, resp = th.Client.EnableBot(createdBot.UserId) + CheckErrorMessage(t, resp, "api.context.permissions.app_error") + }) + + t.Run("enable bot with permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot.UserId) + + _, resp = th.SystemAdminClient.DisableBot(bot.UserId) + CheckOKStatus(t, resp) + + enabledBot1, resp := th.Client.EnableBot(bot.UserId) + CheckOKStatus(t, resp) + bot.UpdateAt = enabledBot1.UpdateAt + bot.DeleteAt = enabledBot1.DeleteAt + require.Equal(t, bot, enabledBot1) + + // Check bot enabled + enab, resp := th.SystemAdminClient.GetBotIncludeDeleted(bot.UserId, "") + CheckOKStatus(t, resp) + require.Zero(t, enab.DeleteAt) + + // Disabling should be idempotent. + enabledBot2, resp := th.Client.EnableBot(bot.UserId) + CheckOKStatus(t, resp) + require.Equal(t, bot, enabledBot2) + }) +} + +func TestAssignBot(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + t.Run("claim non-existent bot", func(t *testing.T) { + _, resp := th.SystemAdminClient.AssignBot(model.NewId(), model.NewId()) + CheckNotFoundStatus(t, resp) + }) + + t.Run("system admin assign bot", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + bot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot.UserId) + + before, resp := th.Client.GetBot(bot.UserId, "") + CheckOKStatus(t, resp) + require.Equal(t, th.BasicUser.Id, before.OwnerId) + + _, resp = th.SystemAdminClient.AssignBot(bot.UserId, th.SystemAdminUser.Id) + CheckOKStatus(t, resp) + + // Original owner doesn't have read others bots permission, therefore can't see bot anymore + _, resp = th.Client.GetBot(bot.UserId, "") + CheckNotFoundStatus(t, resp) + + // System admin can see creator ID has changed + after, resp := th.SystemAdminClient.GetBot(bot.UserId, "") + CheckOKStatus(t, resp) + require.Equal(t, th.SystemAdminUser.Id, after.OwnerId) + + // Assign back to user without permissions to manage + _, resp = th.SystemAdminClient.AssignBot(bot.UserId, th.BasicUser.Id) + CheckOKStatus(t, resp) + + after, resp = th.SystemAdminClient.GetBot(bot.UserId, "") + CheckOKStatus(t, resp) + require.Equal(t, th.BasicUser.Id, after.OwnerId) + }) + + t.Run("random user assign bot", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + th.LoginBasic2() + + // Without permission to read others bots it doesn't exist + _, resp = th.Client.AssignBot(createdBot.UserId, th.BasicUser2.Id) + CheckErrorMessage(t, resp, "store.sql_bot.get.missing.app_error") + + // With permissions to read we don't have permissions to modify + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + _, resp = th.Client.AssignBot(createdBot.UserId, th.BasicUser2.Id) + CheckErrorMessage(t, resp, "api.context.permissions.app_error") + + th.LoginBasic() + }) + + t.Run("delegated user assign bot", func(t *testing.T) { + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + Description: "bot", + } + bot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(bot.UserId) + + // Simulate custom role by just changing the system user role + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_OTHERS_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.SYSTEM_USER_ROLE_ID) + th.LoginBasic2() + + _, resp = th.Client.AssignBot(bot.UserId, th.BasicUser2.Id) + CheckOKStatus(t, resp) + + after, resp := th.SystemAdminClient.GetBot(bot.UserId, "") + CheckOKStatus(t, resp) + require.Equal(t, th.BasicUser2.Id, after.OwnerId) + }) +} + +func sToP(s string) *string { + return &s +} diff --git a/api4/user.go b/api4/user.go index 2216f1f13e..51eb1f846c 100644 --- a/api4/user.go +++ b/api4/user.go @@ -1446,7 +1446,7 @@ func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToUser(c.App.Session, c.Params.UserId) { + if !c.App.SessionHasPermissionToUserOrBot(c.App.Session, c.Params.UserId) { c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) return } @@ -1515,7 +1515,7 @@ func getUserAccessTokensForUser(c *Context, w http.ResponseWriter, r *http.Reque return } - if !c.App.SessionHasPermissionToUser(c.App.Session, c.Params.UserId) { + if !c.App.SessionHasPermissionToUserOrBot(c.App.Session, c.Params.UserId) { c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) return } @@ -1546,7 +1546,7 @@ func getUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToUser(c.App.Session, accessToken.UserId) { + if !c.App.SessionHasPermissionToUserOrBot(c.App.Session, accessToken.UserId) { c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) return } @@ -1575,7 +1575,7 @@ func revokeUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToUser(c.App.Session, accessToken.UserId) { + if !c.App.SessionHasPermissionToUserOrBot(c.App.Session, accessToken.UserId) { c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) return } @@ -1611,7 +1611,7 @@ func disableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) return } - if !c.App.SessionHasPermissionToUser(c.App.Session, accessToken.UserId) { + if !c.App.SessionHasPermissionToUserOrBot(c.App.Session, accessToken.UserId) { c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) return } @@ -1647,7 +1647,7 @@ func enableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !c.App.SessionHasPermissionToUser(c.App.Session, accessToken.UserId) { + if !c.App.SessionHasPermissionToUserOrBot(c.App.Session, accessToken.UserId) { c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) return } diff --git a/api4/user_test.go b/api4/user_test.go index f445b5ed85..80dad8f33a 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -14,6 +14,7 @@ import ( "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/services/mailservice" "github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/utils/testutils" "github.com/stretchr/testify/assert" @@ -371,6 +372,7 @@ func TestGetUser(t *testing.T) { assert.NotNil(t, ruser.Props) assert.Equal(t, ruser.Props["testpropkey"], "testpropvalue") + require.False(t, ruser.IsBot) ruser, resp = th.Client.GetUser(user.Id, resp.Etag) CheckEtag(t, ruser, resp) @@ -415,6 +417,30 @@ func TestGetUser(t *testing.T) { } } +func TestGetBotUser(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + bot := &model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + } + + createdBot, resp := th.Client.CreateBot(bot) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + botUser, resp := th.Client.GetUser(createdBot.UserId, "") + require.Equal(t, bot.Username, botUser.Username) + require.True(t, botUser.IsBot) +} + func TestGetUserByUsername(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -1004,7 +1030,10 @@ func TestGetTotalUsersStat(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() - total := <-th.App.Srv.Store.User().GetTotalUsersCount() + total := <-th.Server.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: false, + IncludeBotAccounts: true, + }) rstats, resp := th.Client.GetTotalUsersStats("") CheckNoError(t, resp) @@ -2020,22 +2049,24 @@ func TestUpdateUserPassword(t *testing.T) { CheckNoError(t, resp) } -/*func TestResetPassword(t *testing.T) { +func TestResetPassword(t *testing.T) { + t.Skip("test disabled during old build server changes, should be investigated") + th := Setup().InitBasic() defer th.TearDown() -th.Client.Logout() + th.Client.Logout() user := th.BasicUser // Delete all the messages before check the reset password mailservice.DeleteMailBox(user.Email) - success, resp :=th.Client.SendPasswordResetEmail(user.Email) + success, resp := th.Client.SendPasswordResetEmail(user.Email) CheckNoError(t, resp) if !success { t.Fatal("should have succeeded") } - _, resp =th.Client.SendPasswordResetEmail("") + _, resp = th.Client.SendPasswordResetEmail("") CheckBadRequestStatus(t, resp) // Should not leak whether the email is attached to an account or not - success, resp =th.Client.SendPasswordResetEmail("notreal@example.com") + success, resp = th.Client.SendPasswordResetEmail("notreal@example.com") CheckNoError(t, resp) if !success { t.Fatal("should have succeeded") @@ -2074,36 +2105,36 @@ th.Client.Logout() } else { recoveryToken = result.Data.(*model.Token) } - _, resp =th.Client.ResetPassword(recoveryToken.Token, "") + _, resp = th.Client.ResetPassword(recoveryToken.Token, "") CheckBadRequestStatus(t, resp) - _, resp =th.Client.ResetPassword(recoveryToken.Token, "newp") + _, resp = th.Client.ResetPassword(recoveryToken.Token, "newp") CheckBadRequestStatus(t, resp) - _, resp =th.Client.ResetPassword("", "newpwd") + _, resp = th.Client.ResetPassword("", "newpwd") CheckBadRequestStatus(t, resp) - _, resp =th.Client.ResetPassword("junk", "newpwd") + _, resp = th.Client.ResetPassword("junk", "newpwd") CheckBadRequestStatus(t, resp) code := "" for i := 0; i < model.TOKEN_SIZE; i++ { code += "a" } - _, resp =th.Client.ResetPassword(code, "newpwd") + _, resp = th.Client.ResetPassword(code, "newpwd") CheckBadRequestStatus(t, resp) - success, resp =th.Client.ResetPassword(recoveryToken.Token, "newpwd") + success, resp = th.Client.ResetPassword(recoveryToken.Token, "newpwd") CheckNoError(t, resp) if !success { t.Fatal("should have succeeded") } -th.Client.Login(user.Email, "newpwd") -th.Client.Logout() - _, resp =th.Client.ResetPassword(recoveryToken.Token, "newpwd") + th.Client.Login(user.Email, "newpwd") + th.Client.Logout() + _, resp = th.Client.ResetPassword(recoveryToken.Token, "newpwd") CheckBadRequestStatus(t, resp) authData := model.NewId() - if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil { + if result := <-th.App.Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil { t.Fatal(result.Err) } - _, resp =th.Client.SendPasswordResetEmail(user.Email) + _, resp = th.Client.SendPasswordResetEmail(user.Email) CheckBadRequestStatus(t, resp) -}*/ +} func TestGetSessions(t *testing.T) { th := Setup().InitBasic() @@ -2371,7 +2402,7 @@ func TestSetProfileImage(t *testing.T) { CheckForbiddenStatus(t, resp) // status code returns either forbidden or unauthorized - // note: forbidden is set as default at th.Client.SetProfileImage when request is terminated early by server + // note: forbidden is set as default at Client4.SetProfileImage when request is terminated early by server th.Client.Logout() _, resp = th.Client.SetProfileImage(user.Id, data) if resp.StatusCode == http.StatusForbidden { @@ -2416,7 +2447,7 @@ func TestSetDefaultProfileImage(t *testing.T) { CheckForbiddenStatus(t, resp) // status code returns either forbidden or unauthorized - // note: forbidden is set as default at th.Client.SetDefaultProfileImage when request is terminated early by server + // note: forbidden is set as default at Client4.SetDefaultProfileImage when request is terminated early by server th.Client.Logout() _, resp = th.Client.SetDefaultProfileImage(user.Id) if resp.StatusCode == http.StatusForbidden { @@ -2440,56 +2471,145 @@ func TestSetDefaultProfileImage(t *testing.T) { } } -func TestCBALogin(t *testing.T) { +func TestLogin(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() th.Client.Logout() - th.App.SetLicense(model.NewTestLicense("saml")) - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ExperimentalSettings.ClientSideCertEnable = true - *cfg.ExperimentalSettings.ClientSideCertCheck = model.CLIENT_SIDE_CERT_CHECK_PRIMARY_AUTH + t.Run("missing password", func(t *testing.T) { + _, resp := th.Client.Login(th.BasicUser.Email, "") + CheckErrorMessage(t, resp, "api.user.login.blank_pwd.app_error") }) - user, resp := th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) - if resp.Error.StatusCode != 400 && user == nil { - t.Fatal("Should have failed because it's missing the cert header") - } - - th.Client.HttpHeader["X-SSL-Client-Cert"] = "valid_cert_fake" - user, resp = th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) - if resp.Error.StatusCode != 400 && user == nil { - t.Fatal("Should have failed because it's missing the cert subject") - } - - th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=mis_match" + th.BasicUser.Email - user, resp = th.Client.Login(th.BasicUser.Email, "") - if resp.Error.StatusCode != 400 && user == nil { - t.Fatal("Should have failed because the emails mismatch") - } - - th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email - user, _ = th.Client.Login(th.BasicUser.Email, "") - if !(user != nil && user.Email == th.BasicUser.Email) { - t.Fatal("Should have been able to login") - } - - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.ExperimentalSettings.ClientSideCertEnable = true - *cfg.ExperimentalSettings.ClientSideCertCheck = model.CLIENT_SIDE_CERT_CHECK_SECONDARY_AUTH + t.Run("unknown user", func(t *testing.T) { + _, resp := th.Client.Login("unknown", th.BasicUser.Password) + CheckErrorMessage(t, resp, "store.sql_user.get_for_login.app_error") }) - th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email - user, _ = th.Client.Login(th.BasicUser.Email, "") - if resp.Error.StatusCode != 400 && user == nil { - t.Fatal("Should have failed because password is required") - } + t.Run("valid login", func(t *testing.T) { + user, resp := th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + CheckNoError(t, resp) + assert.Equal(t, user.Id, th.BasicUser.Id) + }) - th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email - user, _ = th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) - if !(user != nil && user.Email == th.BasicUser.Email) { - t.Fatal("Should have been able to login") - } + t.Run("bot login rejected", func(t *testing.T) { + bot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: "bot", + }) + CheckNoError(t, resp) + + botUser, resp := th.SystemAdminClient.GetUser(bot.UserId, "") + CheckNoError(t, resp) + + changed, resp := th.SystemAdminClient.UpdateUserPassword(bot.UserId, "", "password") + CheckNoError(t, resp) + require.True(t, changed) + + _, resp = th.Client.Login(botUser.Email, "password") + CheckErrorMessage(t, resp, "api.user.login.bot_login_forbidden.app_error") + }) +} + +func TestCBALogin(t *testing.T) { + t.Run("primary", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + th.App.SetLicense(model.NewTestLicense("saml")) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ExperimentalSettings.ClientSideCertEnable = true + *cfg.ExperimentalSettings.ClientSideCertCheck = model.CLIENT_SIDE_CERT_CHECK_PRIMARY_AUTH + }) + + t.Run("missing cert header", func(t *testing.T) { + th.Client.Logout() + _, resp := th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + CheckBadRequestStatus(t, resp) + }) + + t.Run("missing cert subject", func(t *testing.T) { + th.Client.Logout() + th.Client.HttpHeader["X-SSL-Client-Cert"] = "valid_cert_fake" + _, resp := th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + CheckBadRequestStatus(t, resp) + }) + + t.Run("emails mismatch", func(t *testing.T) { + th.Client.Logout() + th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=mis_match" + th.BasicUser.Email + _, resp := th.Client.Login(th.BasicUser.Email, "") + CheckBadRequestStatus(t, resp) + }) + + t.Run("successful cba login", func(t *testing.T) { + th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email + user, resp := th.Client.Login(th.BasicUser.Email, "") + CheckNoError(t, resp) + require.NotNil(t, user) + require.Equal(t, th.BasicUser.Id, user.Id) + }) + + t.Run("bot login rejected", func(t *testing.T) { + bot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: "bot", + }) + CheckNoError(t, resp) + + botUser, resp := th.SystemAdminClient.GetUser(bot.UserId, "") + CheckNoError(t, resp) + + th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + botUser.Email + + _, resp = th.Client.Login(botUser.Email, "") + CheckErrorMessage(t, resp, "api.user.login.bot_login_forbidden.app_error") + }) + }) + + t.Run("secondary", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + th.App.SetLicense(model.NewTestLicense("saml")) + + th.Client.HttpHeader["X-SSL-Client-Cert"] = "valid_cert_fake" + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ExperimentalSettings.ClientSideCertEnable = true + *cfg.ExperimentalSettings.ClientSideCertCheck = model.CLIENT_SIDE_CERT_CHECK_SECONDARY_AUTH + }) + + t.Run("password required", func(t *testing.T) { + th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email + _, resp := th.Client.Login(th.BasicUser.Email, "") + CheckBadRequestStatus(t, resp) + }) + + t.Run("successful cba login with password", func(t *testing.T) { + th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + th.BasicUser.Email + user, resp := th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + CheckNoError(t, resp) + require.NotNil(t, user) + require.Equal(t, th.BasicUser.Id, user.Id) + }) + + t.Run("bot login rejected", func(t *testing.T) { + bot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: "bot", + }) + CheckNoError(t, resp) + + botUser, resp := th.SystemAdminClient.GetUser(bot.UserId, "") + CheckNoError(t, resp) + + changed, resp := th.SystemAdminClient.UpdateUserPassword(bot.UserId, "", "password") + CheckNoError(t, resp) + require.True(t, changed) + + th.Client.HttpHeader["X-SSL-Client-Cert-Subject-DN"] = "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=" + botUser.Email + + _, resp = th.Client.Login(botUser.Email, "password") + CheckErrorMessage(t, resp, "api.user.login.bot_login_forbidden.app_error") + }) + }) } func TestSwitchAccount(t *testing.T) { @@ -2619,165 +2739,472 @@ func TestSwitchAccount(t *testing.T) { CheckUnauthorizedStatus(t, resp) } -func TestCreateUserAccessToken(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() - - testDescription := "test token" - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - - _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckForbiddenStatus(t, resp) - - _, resp = th.Client.CreateUserAccessToken("notarealuserid", testDescription) - CheckBadRequestStatus(t, resp) - - _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, "") - CheckBadRequestStatus(t, resp) - - th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) - - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = false }) - _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNotImplementedStatus(t, resp) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - - rtoken, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNoError(t, resp) - - if rtoken.UserId != th.BasicUser.Id { - t.Fatal("wrong user id") - } else if rtoken.Token == "" { - t.Fatal("token should not be empty") - } else if rtoken.Id == "" { - t.Fatal("id should not be empty") - } else if rtoken.Description != testDescription { - t.Fatal("description did not match") - } else if !rtoken.IsActive { - t.Fatal("token should be active") - } +func assertToken(t *testing.T, th *TestHelper, token *model.UserAccessToken, expectedUserId string) { + t.Helper() oldSessionToken := th.Client.AuthToken - th.Client.AuthToken = rtoken.Token + defer func() { th.Client.AuthToken = oldSessionToken }() + + th.Client.AuthToken = token.Token ruser, resp := th.Client.GetMe("") CheckNoError(t, resp) - if ruser.Id != th.BasicUser.Id { - t.Fatal("returned wrong user") - } + assert.Equal(t, expectedUserId, ruser.Id, "returned wrong user") +} - th.Client.AuthToken = oldSessionToken +func assertInvalidToken(t *testing.T, th *TestHelper, token *model.UserAccessToken) { + t.Helper() - _, resp = th.Client.CreateUserAccessToken(th.BasicUser2.Id, testDescription) - CheckForbiddenStatus(t, resp) + oldSessionToken := th.Client.AuthToken + defer func() { th.Client.AuthToken = oldSessionToken }() - rtoken, resp = th.SystemAdminClient.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNoError(t, resp) + th.Client.AuthToken = token.Token + _, resp := th.Client.GetMe("") + CheckUnauthorizedStatus(t, resp) +} - if rtoken.UserId != th.BasicUser.Id { - t.Fatal("wrong user id") - } +func TestCreateUserAccessToken(t *testing.T) { + t.Run("create token without permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - oldSessionToken = th.Client.AuthToken - th.Client.AuthToken = rtoken.Token - ruser, resp = th.Client.GetMe("") - CheckNoError(t, resp) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - if ruser.Id != th.BasicUser.Id { - t.Fatal("returned wrong user") - } + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckForbiddenStatus(t, resp) + }) - th.Client.AuthToken = oldSessionToken + t.Run("create token for invalid user id", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - session, _ := th.App.GetSession(th.Client.AuthToken) - session.IsOAuth = true - th.App.AddSessionToCache(session) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckForbiddenStatus(t, resp) + _, resp := th.Client.CreateUserAccessToken("notarealuserid", "test token") + CheckBadRequestStatus(t, resp) + }) + + t.Run("create token with invalid value", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "") + CheckBadRequestStatus(t, resp) + }) + + t.Run("create token with user access tokens disabled", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = false }) + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNotImplementedStatus(t, resp) + }) + + t.Run("create user access token", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + rtoken, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + + assert.Equal(t, th.BasicUser.Id, rtoken.UserId, "wrong user id") + assert.NotEmpty(t, rtoken.Token, "token should not be empty") + assert.NotEmpty(t, rtoken.Id, "id should not be empty") + assert.Equal(t, "test token", rtoken.Description, "description did not match") + assert.True(t, rtoken.IsActive, "token should be active") + + assertToken(t, th, rtoken, th.BasicUser.Id) + }) + + t.Run("create user access token as second user, without permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser2.Id, "test token") + CheckForbiddenStatus(t, resp) + }) + + t.Run("create user access token for basic user as as system admin", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + rtoken, resp := th.SystemAdminClient.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + assert.Equal(t, th.BasicUser.Id, rtoken.UserId) + + oldSessionToken := th.Client.AuthToken + defer func() { th.Client.AuthToken = oldSessionToken }() + + assertToken(t, th, rtoken, th.BasicUser.Id) + }) + + t.Run("create access token as oauth session", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + session, _ := th.App.GetSession(th.Client.AuthToken) + session.IsOAuth = true + th.App.AddSessionToCache(session) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckForbiddenStatus(t, resp) + }) + + t.Run("create access token for bot created by user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + t.Run("without MANAGE_BOT permission", func(t *testing.T) { + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + _, resp = th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + token, resp := th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + assert.Equal(t, createdBot.UserId, token.UserId) + assertToken(t, th, token, createdBot.UserId) + }) + }) + + t.Run("create access token for bot created by another user, only having MANAGE_BOTS permission", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + t.Run("only having MANAGE_BOTS permission", func(t *testing.T) { + _, resp = th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + + rtoken, resp := th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + assert.Equal(t, createdBot.UserId, rtoken.UserId) + + assertToken(t, th, rtoken, createdBot.UserId) + }) + }) } func TestGetUserAccessToken(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() + t.Run("get for invalid user id", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - testDescription := "test token" + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + _, resp := th.Client.GetUserAccessToken("123") + CheckBadRequestStatus(t, resp) + }) - _, resp := th.Client.GetUserAccessToken("123") - CheckBadRequestStatus(t, resp) + t.Run("get for unknown user id", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - _, resp = th.Client.GetUserAccessToken(model.NewId()) - CheckForbiddenStatus(t, resp) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) - token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNoError(t, resp) + _, resp := th.Client.GetUserAccessToken(model.NewId()) + CheckForbiddenStatus(t, resp) + }) - rtoken, resp := th.Client.GetUserAccessToken(token.Id) - CheckNoError(t, resp) + t.Run("get my token", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if rtoken.UserId != th.BasicUser.Id { - t.Fatal("wrong user id") - } else if rtoken.Token != "" { - t.Fatal("token should be blank") - } else if rtoken.Id == "" { - t.Fatal("id should not be empty") - } else if rtoken.Description != testDescription { - t.Fatal("description did not match") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) - _, resp = th.SystemAdminClient.GetUserAccessToken(token.Id) - CheckNoError(t, resp) + token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) - _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNoError(t, resp) + rtoken, resp := th.Client.GetUserAccessToken(token.Id) + CheckNoError(t, resp) - rtokens, resp := th.Client.GetUserAccessTokensForUser(th.BasicUser.Id, 0, 100) - CheckNoError(t, resp) + assert.Equal(t, th.BasicUser.Id, rtoken.UserId, "wrong user id") + assert.Empty(t, rtoken.Token, "token should be blank") + assert.NotEmpty(t, rtoken.Id, "id should not be empty") + assert.Equal(t, "test token", rtoken.Description, "description did not match") + }) - if len(rtokens) != 2 { - t.Fatal("should have 2 tokens") - } + t.Run("get user token as system admin", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - for _, uat := range rtokens { - if uat.UserId != th.BasicUser.Id { - t.Fatal("wrong user id") + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + + rtoken, resp := th.SystemAdminClient.GetUserAccessToken(token.Id) + CheckNoError(t, resp) + + assert.Equal(t, th.BasicUser.Id, rtoken.UserId, "wrong user id") + assert.Empty(t, rtoken.Token, "token should be blank") + assert.NotEmpty(t, rtoken.Id, "id should not be empty") + assert.Equal(t, "test token", rtoken.Description, "description did not match") + }) + + t.Run("get token for bot created by user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + t.Run("without MANAGE_BOTS permission", func(t *testing.T) { + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + _, resp := th.Client.GetUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + returnedToken, resp := th.Client.GetUserAccessToken(token.Id) + CheckNoError(t, resp) + + // Actual token won't be returned. + returnedToken.Token = token.Token + assert.Equal(t, token, returnedToken) + }) + }) + + t.Run("get token for bot created by another user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.SystemAdminClient.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + t.Run("only having MANAGE_BOTS permission", func(t *testing.T) { + _, resp = th.Client.GetUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + + returnedToken, resp := th.Client.GetUserAccessToken(token.Id) + CheckNoError(t, resp) + + // Actual token won't be returned. + returnedToken.Token = token.Token + assert.Equal(t, token, returnedToken) + }) + }) +} + +func TestGetUserAccessTokensForUser(t *testing.T) { + t.Run("multiple tokens, offset 0, limit 100", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + + _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token 2") + CheckNoError(t, resp) + + rtokens, resp := th.Client.GetUserAccessTokensForUser(th.BasicUser.Id, 0, 100) + CheckNoError(t, resp) + + assert.Len(t, rtokens, 2, "should have 2 tokens") + for _, uat := range rtokens { + assert.Equal(t, th.BasicUser.Id, uat.UserId, "wrong user id") } - } + }) - rtokens, resp = th.Client.GetUserAccessTokensForUser(th.BasicUser.Id, 1, 1) - CheckNoError(t, resp) + t.Run("multiple tokens as system admin, offset 0, limit 100", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if len(rtokens) != 1 { - t.Fatal("should have 1 token") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - rtokens, resp = th.SystemAdminClient.GetUserAccessTokensForUser(th.BasicUser.Id, 0, 100) - CheckNoError(t, resp) + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) - if len(rtokens) != 2 { - t.Fatal("should have 2 tokens") - } + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) - _, resp = th.Client.GetUserAccessTokens(0, 100) - CheckForbiddenStatus(t, resp) + _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token 2") + CheckNoError(t, resp) - rtokens, resp = th.SystemAdminClient.GetUserAccessTokens(1, 1) - CheckNoError(t, resp) + rtokens, resp := th.Client.GetUserAccessTokensForUser(th.BasicUser.Id, 0, 100) + CheckNoError(t, resp) - if len(rtokens) != 1 { - t.Fatal("should have 1 token") - } + assert.Len(t, rtokens, 2, "should have 2 tokens") + for _, uat := range rtokens { + assert.Equal(t, th.BasicUser.Id, uat.UserId, "wrong user id") + } + }) - rtokens, resp = th.SystemAdminClient.GetUserAccessTokens(0, 2) - CheckNoError(t, resp) + t.Run("multiple tokens, offset 1, limit 1", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if len(rtokens) != 2 { - t.Fatal("should have 2 tokens") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + + _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token 2") + CheckNoError(t, resp) + + rtokens, resp := th.Client.GetUserAccessTokensForUser(th.BasicUser.Id, 1, 1) + CheckNoError(t, resp) + + assert.Len(t, rtokens, 1, "should have 1 tokens") + for _, uat := range rtokens { + assert.Equal(t, th.BasicUser.Id, uat.UserId, "wrong user id") + } + }) +} + +func TestGetUserAccessTokens(t *testing.T) { + t.Run("GetUserAccessTokens, not a system admin", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + _, resp := th.Client.GetUserAccessTokens(0, 100) + CheckForbiddenStatus(t, resp) + }) + + t.Run("GetUserAccessTokens, as a system admin, page 1, perPage 1", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token 2") + CheckNoError(t, resp) + + _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token 2") + CheckNoError(t, resp) + + rtokens, resp := th.SystemAdminClient.GetUserAccessTokens(1, 1) + CheckNoError(t, resp) + + assert.Len(t, rtokens, 1, "should have 1 token") + }) + + t.Run("GetUserAccessTokens, as a system admin, page 0, perPage 2", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + + _, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token 2") + CheckNoError(t, resp) + + _, resp = th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token 2") + CheckNoError(t, resp) + + rtokens, resp := th.SystemAdminClient.GetUserAccessTokens(0, 2) + CheckNoError(t, resp) + + assert.Len(t, rtokens, 2, "should have 2 tokens") + }) } func TestSearchUserAccessToken(t *testing.T) { @@ -2825,128 +3252,357 @@ func TestSearchUserAccessToken(t *testing.T) { } func TestRevokeUserAccessToken(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() + t.Run("revoke user token", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - testDescription := "test token" + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + assertToken(t, th, token, th.BasicUser.Id) - th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) - token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNoError(t, resp) + ok, resp := th.Client.RevokeUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") - oldSessionToken := th.Client.AuthToken - th.Client.AuthToken = token.Token - _, resp = th.Client.GetMe("") - CheckNoError(t, resp) - th.Client.AuthToken = oldSessionToken + assertInvalidToken(t, th, token) + }) - ok, resp := th.Client.RevokeUserAccessToken(token.Id) - CheckNoError(t, resp) + t.Run("revoke token belonging to another user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if !ok { - t.Fatal("should have passed") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - oldSessionToken = th.Client.AuthToken - th.Client.AuthToken = token.Token - _, resp = th.Client.GetMe("") - CheckUnauthorizedStatus(t, resp) - th.Client.AuthToken = oldSessionToken + token, resp := th.SystemAdminClient.CreateUserAccessToken(th.BasicUser2.Id, "test token") + CheckNoError(t, resp) - token, resp = th.SystemAdminClient.CreateUserAccessToken(th.BasicUser2.Id, testDescription) - CheckNoError(t, resp) + ok, resp := th.Client.RevokeUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + assert.False(t, ok, "should have failed") + }) - ok, resp = th.Client.RevokeUserAccessToken(token.Id) - CheckForbiddenStatus(t, resp) + t.Run("revoke token for bot created by user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if ok { - t.Fatal("should have failed") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + t.Run("without MANAGE_BOTS permission", func(t *testing.T) { + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + _, resp := th.Client.RevokeUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + ok, resp := th.Client.RevokeUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + }) + }) + + t.Run("revoke token for bot created by another user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.SystemAdminClient.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + t.Run("only having MANAGE_BOTS permission", func(t *testing.T) { + _, resp = th.Client.RevokeUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + + ok, resp := th.Client.RevokeUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + }) + }) } func TestDisableUserAccessToken(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() + t.Run("disable user token", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - testDescription := "test token" + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - *th.App.Config().ServiceSettings.EnableUserAccessTokens = true + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + assertToken(t, th, token, th.BasicUser.Id) - th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) - token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNoError(t, resp) + ok, resp := th.Client.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") - oldSessionToken := th.Client.AuthToken - th.Client.AuthToken = token.Token - _, resp = th.Client.GetMe("") - CheckNoError(t, resp) - th.Client.AuthToken = oldSessionToken + assertInvalidToken(t, th, token) + }) - ok, resp := th.Client.DisableUserAccessToken(token.Id) - CheckNoError(t, resp) + t.Run("disable token belonging to another user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if !ok { - t.Fatal("should have passed") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - oldSessionToken = th.Client.AuthToken - th.Client.AuthToken = token.Token - _, resp = th.Client.GetMe("") - CheckUnauthorizedStatus(t, resp) - th.Client.AuthToken = oldSessionToken + token, resp := th.SystemAdminClient.CreateUserAccessToken(th.BasicUser2.Id, "test token") + CheckNoError(t, resp) - token, resp = th.SystemAdminClient.CreateUserAccessToken(th.BasicUser2.Id, testDescription) - CheckNoError(t, resp) + ok, resp := th.Client.DisableUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + assert.False(t, ok, "should have failed") + }) - ok, resp = th.Client.DisableUserAccessToken(token.Id) - CheckForbiddenStatus(t, resp) + t.Run("disable token for bot created by user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if ok { - t.Fatal("should have failed") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + t.Run("without MANAGE_BOTS permission", func(t *testing.T) { + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + _, resp := th.Client.DisableUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + ok, resp := th.Client.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + }) + }) + + t.Run("disable token for bot created by another user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.SystemAdminClient.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + t.Run("only having MANAGE_BOTS permission", func(t *testing.T) { + _, resp = th.Client.DisableUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + + ok, resp := th.Client.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + }) + }) } func TestEnableUserAccessToken(t *testing.T) { - th := Setup().InitBasic() - defer th.TearDown() + t.Run("enable user token", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - testDescription := "test token" + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - *th.App.Config().ServiceSettings.EnableUserAccessTokens = true + th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) + token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, "test token") + CheckNoError(t, resp) + assertToken(t, th, token, th.BasicUser.Id) - th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false) - token, resp := th.Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) - CheckNoError(t, resp) + ok, resp := th.Client.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") - oldSessionToken := th.Client.AuthToken - th.Client.AuthToken = token.Token - _, resp = th.Client.GetMe("") - CheckNoError(t, resp) - th.Client.AuthToken = oldSessionToken + assertInvalidToken(t, th, token) - _, resp = th.Client.DisableUserAccessToken(token.Id) - CheckNoError(t, resp) + ok, resp = th.Client.EnableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") - oldSessionToken = th.Client.AuthToken - th.Client.AuthToken = token.Token - _, resp = th.Client.GetMe("") - CheckUnauthorizedStatus(t, resp) - th.Client.AuthToken = oldSessionToken + assertToken(t, th, token, th.BasicUser.Id) + }) - ok, resp := th.Client.EnableUserAccessToken(token.Id) - CheckNoError(t, resp) + t.Run("enable token belonging to another user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() - if !ok { - t.Fatal("should have passed") - } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) - oldSessionToken = th.Client.AuthToken - th.Client.AuthToken = token.Token - _, resp = th.Client.GetMe("") - CheckNoError(t, resp) - th.Client.AuthToken = oldSessionToken + token, resp := th.SystemAdminClient.CreateUserAccessToken(th.BasicUser2.Id, "test token") + CheckNoError(t, resp) + + ok, resp := th.SystemAdminClient.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + + ok, resp = th.Client.DisableUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + assert.False(t, ok, "should have failed") + }) + + t.Run("enable token for bot created by user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.Client.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.Client.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + ok, resp := th.Client.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + + t.Run("without MANAGE_BOTS permission", func(t *testing.T) { + th.RemovePermissionFromRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + _, resp := th.Client.EnableUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + + ok, resp := th.Client.EnableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + }) + }) + + t.Run("enable token for bot created by another user", func(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + defer th.RestoreDefaultRolePermissions(th.SaveDefaultRolePermissions()) + th.AddPermissionToRole(model.PERMISSION_CREATE_BOT.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_MANAGE_BOTS.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.AddPermissionToRole(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, model.TEAM_USER_ROLE_ID) + th.App.UpdateUserRoles(th.BasicUser.Id, model.TEAM_USER_ROLE_ID, false) + + createdBot, resp := th.SystemAdminClient.CreateBot(&model.Bot{ + Username: GenerateTestUsername(), + DisplayName: "a bot", + Description: "bot", + }) + CheckCreatedStatus(t, resp) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + token, resp := th.SystemAdminClient.CreateUserAccessToken(createdBot.UserId, "test token") + CheckNoError(t, resp) + + ok, resp := th.SystemAdminClient.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + + t.Run("only having MANAGE_BOTS permission", func(t *testing.T) { + _, resp := th.Client.EnableUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + }) + + t.Run("with MANAGE_OTHERS_BOTS permission", func(t *testing.T) { + th.AddPermissionToRole(model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.TEAM_USER_ROLE_ID) + + ok, resp := th.Client.EnableUserAccessToken(token.Id) + CheckNoError(t, resp) + assert.True(t, ok, "should have passed") + }) + }) } func TestUserAccessTokenInactiveUser(t *testing.T) { diff --git a/app/analytics.go b/app/analytics.go index e441696448..813a573dfe 100644 --- a/app/analytics.go +++ b/app/analytics.go @@ -19,7 +19,7 @@ const ( func (a *App) GetAnalytics(name string, teamId string) (model.AnalyticsRows, *model.AppError) { skipIntensiveQueries := false var systemUserCount int64 - r := <-a.Srv.Store.User().AnalyticsUniqueUserCount("") + r := <-a.Srv.Store.User().Count(model.UserCountOptions{}) if r.Err != nil { return nil, r.Err } @@ -53,7 +53,9 @@ func (a *App) GetAnalytics(name string, teamId string) (model.AnalyticsRows, *mo if teamId == "" { userInactiveChan = a.Srv.Store.User().AnalyticsGetInactiveUsersCount() } else { - userChan = a.Srv.Store.User().AnalyticsUniqueUserCount(teamId) + userChan = a.Srv.Store.User().Count(model.UserCountOptions{ + TeamId: teamId, + }) } var postChan store.StoreChannel diff --git a/app/app_test.go b/app/app_test.go index dc0b7919bb..1dad5baa71 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -163,6 +163,11 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + model.PERMISSION_CREATE_BOT.Id, + model.PERMISSION_READ_BOTS.Id, + model.PERMISSION_READ_OTHERS_BOTS.Id, + model.PERMISSION_MANAGE_BOTS.Id, + model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.PERMISSION_REMOVE_OTHERS_REACTIONS.Id, model.PERMISSION_LIST_TEAM_CHANNELS.Id, model.PERMISSION_JOIN_PUBLIC_CHANNELS.Id, @@ -331,6 +336,11 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) { model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + model.PERMISSION_CREATE_BOT.Id, + model.PERMISSION_READ_BOTS.Id, + model.PERMISSION_READ_OTHERS_BOTS.Id, + model.PERMISSION_MANAGE_BOTS.Id, + model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.PERMISSION_REMOVE_OTHERS_REACTIONS.Id, model.PERMISSION_LIST_TEAM_CHANNELS.Id, model.PERMISSION_JOIN_PUBLIC_CHANNELS.Id, @@ -465,6 +475,11 @@ func TestDoEmojisPermissionsMigration(t *testing.T) { model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, model.PERMISSION_READ_USER_ACCESS_TOKEN.Id, model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + model.PERMISSION_CREATE_BOT.Id, + model.PERMISSION_READ_BOTS.Id, + model.PERMISSION_READ_OTHERS_BOTS.Id, + model.PERMISSION_MANAGE_BOTS.Id, + model.PERMISSION_MANAGE_OTHERS_BOTS.Id, model.PERMISSION_REMOVE_OTHERS_REACTIONS.Id, model.PERMISSION_LIST_TEAM_CHANNELS.Id, model.PERMISSION_JOIN_PUBLIC_CHANNELS.Id, diff --git a/app/authentication.go b/app/authentication.go index 7278813d3e..673229a723 100644 --- a/app/authentication.go +++ b/app/authentication.go @@ -143,6 +143,10 @@ func (a *App) CheckUserPreflightAuthenticationCriteria(user *model.User, mfaToke return err } + if err := checkUserNotBot(user); err != nil { + return err + } + if err := checkUserLoginAttempts(user, *a.Config().ServiceSettings.MaximumLoginAttempts); err != nil { return err } @@ -191,6 +195,13 @@ func checkUserNotDisabled(user *model.User) *model.AppError { return nil } +func checkUserNotBot(user *model.User) *model.AppError { + if user.IsBot { + return model.NewAppError("Login", "api.user.login.bot_login_forbidden.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) + } + return nil +} + func (a *App) authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) { license := a.License() ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil && license != nil && *license.Features.LDAP diff --git a/app/authorization.go b/app/authorization.go index 3310237c62..74f77a97bf 100644 --- a/app/authorization.go +++ b/app/authorization.go @@ -12,6 +12,10 @@ import ( "github.com/mattermost/mattermost-server/model" ) +func (a *App) MakePermissionError(permission *model.Permission) *model.AppError { + return model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+a.Session.UserId+", "+"permission="+permission.Id, http.StatusForbidden) +} + func (a *App) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool { return a.RolesGrantPermission(session.GetUserRoles(), permission.Id) } @@ -98,6 +102,18 @@ func (a *App) SessionHasPermissionToUser(session model.Session, userId string) b return false } +func (a *App) SessionHasPermissionToUserOrBot(session model.Session, userId string) bool { + if a.SessionHasPermissionToUser(session, userId) { + return true + } + + if err := a.SessionHasPermissionToManageBot(session, userId); err == nil { + return true + } + + return false +} + func (a *App) HasPermissionTo(askingUserId string, permission *model.Permission) bool { user, err := a.GetUser(askingUserId) if err != nil { @@ -205,3 +221,35 @@ func (a *App) RolesGrantPermission(roleNames []string, permissionId string) bool return false } + +// SessionHasPermissionToManageBot returns nil if the session has access to manage the given bot. +// This function deviates from other authorization checks in returning an error instead of just +// a boolean, allowing the permission failure to be exposed with more granularity. +func (a *App) SessionHasPermissionToManageBot(session model.Session, botUserId string) *model.AppError { + existingBot, err := a.GetBot(botUserId, true) + if err != nil { + return err + } + + if existingBot.OwnerId == session.UserId { + if !a.SessionHasPermissionTo(session, model.PERMISSION_MANAGE_BOTS) { + if !a.SessionHasPermissionTo(session, model.PERMISSION_READ_BOTS) { + // If the user doesn't have permission to read bots, pretend as if + // the bot doesn't exist at all. + return model.MakeBotNotFoundError(botUserId) + } + return a.MakePermissionError(model.PERMISSION_MANAGE_BOTS) + } + } else { + if !a.SessionHasPermissionTo(session, model.PERMISSION_MANAGE_OTHERS_BOTS) { + if !a.SessionHasPermissionTo(session, model.PERMISSION_READ_OTHERS_BOTS) { + // If the user doesn't have permission to read others' bots, + // pretend as if the bot doesn't exist at all. + return model.MakeBotNotFoundError(botUserId) + } + return a.MakePermissionError(model.PERMISSION_MANAGE_OTHERS_BOTS) + } + } + + return nil +} diff --git a/app/bot.go b/app/bot.go new file mode 100644 index 0000000000..fac6d5747b --- /dev/null +++ b/app/bot.go @@ -0,0 +1,180 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" +) + +// CreateBot creates the given bot and corresponding user. +func (a *App) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) { + result := <-a.Srv.Store.User().Save(model.UserFromBot(bot)) + if result.Err != nil { + return nil, result.Err + } + bot.UserId = result.Data.(*model.User).Id + + result = <-a.Srv.Store.Bot().Save(bot) + if result.Err != nil { + <-a.Srv.Store.User().PermanentDelete(bot.UserId) + return nil, result.Err + } + + return result.Data.(*model.Bot), nil +} + +// PatchBot applies the given patch to the bot and corresponding user. +func (a *App) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) { + bot, err := a.GetBot(botUserId, true) + if err != nil { + return nil, err + } + + bot.Patch(botPatch) + + result := <-a.Srv.Store.User().Get(botUserId) + if result.Err != nil { + return nil, result.Err + } + user := result.Data.(*model.User) + + patchedUser := model.UserFromBot(bot) + user.Id = patchedUser.Id + user.Username = patchedUser.Username + user.Email = patchedUser.Email + user.FirstName = patchedUser.FirstName + if result = <-a.Srv.Store.User().Update(user, true); result.Err != nil { + return nil, result.Err + } + + result = <-a.Srv.Store.Bot().Update(bot) + if result.Err != nil { + return nil, result.Err + } + + return result.Data.(*model.Bot), nil +} + +// GetBot returns the given bot. +func (a *App) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) { + result := <-a.Srv.Store.Bot().Get(botUserId, includeDeleted) + if result.Err != nil { + return nil, result.Err + } + + return result.Data.(*model.Bot), nil +} + +// GetBots returns the requested page of bots. +func (a *App) GetBots(options *model.BotGetOptions) (model.BotList, *model.AppError) { + result := <-a.Srv.Store.Bot().GetAll(options) + if result.Err != nil { + return nil, result.Err + } + + return result.Data.([]*model.Bot), nil +} + +// UpdateBotActive marks a bot as active or inactive, along with its corresponding user. +func (a *App) UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) { + result := <-a.Srv.Store.User().Get(botUserId) + if result.Err != nil { + return nil, result.Err + } + user := result.Data.(*model.User) + + if _, err := a.UpdateActive(user, active); err != nil { + return nil, err + } + + result = <-a.Srv.Store.Bot().Get(botUserId, true) + if result.Err != nil { + return nil, result.Err + } + bot := result.Data.(*model.Bot) + + changed := true + if active && bot.DeleteAt != 0 { + bot.DeleteAt = 0 + } else if !active && bot.DeleteAt == 0 { + bot.DeleteAt = model.GetMillis() + } else { + changed = false + } + + if changed { + result := <-a.Srv.Store.Bot().Update(bot) + if result.Err != nil { + return nil, result.Err + } + bot = result.Data.(*model.Bot) + } + + return bot, nil +} + +// PermanentDeleteBot permanently deletes a bot and its corresponding user. +func (a *App) PermanentDeleteBot(botUserId string) *model.AppError { + if result := <-a.Srv.Store.Bot().PermanentDelete(botUserId); result.Err != nil { + return result.Err + } + + if result := <-a.Srv.Store.User().PermanentDelete(botUserId); result.Err != nil { + return result.Err + } + + return nil +} + +// UpdateBotOwner changes a bot's owner to the given value +func (a *App) UpdateBotOwner(botUserId, newOwnerId string) (*model.Bot, *model.AppError) { + result := <-a.Srv.Store.Bot().Get(botUserId, true) + if result.Err != nil { + return nil, result.Err + } + bot := result.Data.(*model.Bot) + + bot.OwnerId = newOwnerId + + if result = <-a.Srv.Store.Bot().Update(bot); result.Err != nil { + return nil, result.Err + } + + return result.Data.(*model.Bot), nil +} + +// disableUserBots disables all bots owned by the given user +func (a *App) disableUserBots(userId string) *model.AppError { + perPage := 20 + for { + options := &model.BotGetOptions{ + OwnerId: userId, + IncludeDeleted: false, + OnlyOrphaned: false, + Page: 0, + PerPage: perPage, + } + userBots, err := a.GetBots(options) + if err != nil { + return err + } + + for _, bot := range userBots { + _, err := a.UpdateBotActive(bot.UserId, false) + if err != nil { + mlog.Error("Unable to deactivate bot.", mlog.String("bot_user_id", bot.UserId), mlog.Err(err)) + } + } + + // Get next set of bots if we got the max number of bots + if len(userBots) == perPage { + options.Page += 1 + continue + } + break + } + + return nil +} diff --git a/app/bot_test.go b/app/bot_test.go new file mode 100644 index 0000000000..60b95ece93 --- /dev/null +++ b/app/bot_test.go @@ -0,0 +1,549 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" +) + +func TestCreateBot(t *testing.T) { + t.Run("invalid bot", func(t *testing.T) { + t.Run("relative to user", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.App.CreateBot(&model.Bot{ + Username: "invalid username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.NotNil(t, err) + require.Equal(t, "model.user.is_valid.username.app_error", err.Id) + }) + + t.Run("relative to bot", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: strings.Repeat("x", 1025), + OwnerId: th.BasicUser.Id, + }) + require.NotNil(t, err) + require.Equal(t, "model.bot.is_valid.description.app_error", err.Id) + }) + }) + + t.Run("create bot", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot.UserId) + assert.Equal(t, "username", bot.Username) + assert.Equal(t, "a bot", bot.Description) + assert.Equal(t, th.BasicUser.Id, bot.OwnerId) + }) + + t.Run("create bot, username already used by a non-bot user", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.App.CreateBot(&model.Bot{ + Username: th.BasicUser.Username, + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.NotNil(t, err) + require.Equal(t, "store.sql_user.save.username_exists.app_error", err.Id) + }) +} + +func TestPatchBot(t *testing.T) { + t.Run("invalid patch for user", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP("invalid username"), + DisplayName: sToP("an updated bot"), + Description: sToP("updated bot"), + } + + _, err = th.App.PatchBot(bot.UserId, botPatch) + require.NotNil(t, err) + require.Equal(t, "model.user.is_valid.username.app_error", err.Id) + }) + + t.Run("invalid patch for bot", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP("username"), + DisplayName: sToP("display name"), + Description: sToP(strings.Repeat("x", 1025)), + } + + _, err = th.App.PatchBot(bot.UserId, botPatch) + require.NotNil(t, err) + require.Equal(t, "model.bot.is_valid.description.app_error", err.Id) + }) + + t.Run("patch bot", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot := &model.Bot{ + Username: "username", + DisplayName: "bot", + Description: "a bot", + OwnerId: th.BasicUser.Id, + } + + createdBot, err := th.App.CreateBot(bot) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(createdBot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP("username2"), + DisplayName: sToP("updated bot"), + Description: sToP("an updated bot"), + } + + patchedBot, err := th.App.PatchBot(createdBot.UserId, botPatch) + require.Nil(t, err) + + createdBot.Username = "username2" + createdBot.DisplayName = "updated bot" + createdBot.Description = "an updated bot" + createdBot.UpdateAt = patchedBot.UpdateAt + require.Equal(t, createdBot, patchedBot) + }) + + t.Run("patch bot, username already used by a non-bot user", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "username", + DisplayName: "bot", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot.UserId) + + botPatch := &model.BotPatch{ + Username: sToP(th.BasicUser2.Username), + } + + _, err = th.App.PatchBot(bot.UserId, botPatch) + require.NotNil(t, err) + require.Equal(t, "store.sql_user.update.username_taken.app_error", err.Id) + }) +} + +func TestGetBot(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot1, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot1.UserId) + + bot2, err := th.App.CreateBot(&model.Bot{ + Username: "username2", + Description: "a second bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot2.UserId) + + deletedBot, err := th.App.CreateBot(&model.Bot{ + Username: "username3", + Description: "a deleted bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + deletedBot, err = th.App.UpdateBotActive(deletedBot.UserId, false) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(deletedBot.UserId) + + t.Run("get unknown bot", func(t *testing.T) { + _, err := th.App.GetBot(model.NewId(), false) + require.NotNil(t, err) + require.Equal(t, "store.sql_bot.get.missing.app_error", err.Id) + }) + + t.Run("get bot1", func(t *testing.T) { + bot, err := th.App.GetBot(bot1.UserId, false) + require.Nil(t, err) + assert.Equal(t, bot1, bot) + }) + + t.Run("get bot2", func(t *testing.T) { + bot, err := th.App.GetBot(bot2.UserId, false) + require.Nil(t, err) + assert.Equal(t, bot2, bot) + }) + + t.Run("get deleted bot", func(t *testing.T) { + _, err := th.App.GetBot(deletedBot.UserId, false) + require.NotNil(t, err) + require.Equal(t, "store.sql_bot.get.missing.app_error", err.Id) + }) + + t.Run("get deleted bot, include deleted", func(t *testing.T) { + bot, err := th.App.GetBot(deletedBot.UserId, true) + require.Nil(t, err) + assert.Equal(t, deletedBot, bot) + }) +} + +func TestGetBots(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + OwnerId1 := model.NewId() + OwnerId2 := model.NewId() + + bot1, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: OwnerId1, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot1.UserId) + + deletedBot1, err := th.App.CreateBot(&model.Bot{ + Username: "username4", + Description: "a deleted bot", + OwnerId: OwnerId1, + }) + require.Nil(t, err) + deletedBot1, err = th.App.UpdateBotActive(deletedBot1.UserId, false) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(deletedBot1.UserId) + + bot2, err := th.App.CreateBot(&model.Bot{ + Username: "username2", + Description: "a second bot", + OwnerId: OwnerId1, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot2.UserId) + + bot3, err := th.App.CreateBot(&model.Bot{ + Username: "username3", + Description: "a third bot", + OwnerId: OwnerId1, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot3.UserId) + + bot4, err := th.App.CreateBot(&model.Bot{ + Username: "username5", + Description: "a fourth bot", + OwnerId: OwnerId2, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot4.UserId) + + deletedBot2, err := th.App.CreateBot(&model.Bot{ + Username: "username6", + Description: "a deleted bot", + OwnerId: OwnerId2, + }) + require.Nil(t, err) + deletedBot2, err = th.App.UpdateBotActive(deletedBot2.UserId, false) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(deletedBot2.UserId) + + t.Run("get bots, page=0, perPage=10", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 10, + OwnerId: "", + IncludeDeleted: false, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{bot1, bot2, bot3, bot4}, bots) + }) + + t.Run("get bots, page=0, perPage=1", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 1, + OwnerId: "", + IncludeDeleted: false, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{bot1}, bots) + }) + + t.Run("get bots, page=1, perPage=2", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 1, + PerPage: 2, + OwnerId: "", + IncludeDeleted: false, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{bot3, bot4}, bots) + }) + + t.Run("get bots, page=2, perPage=2", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 2, + PerPage: 2, + OwnerId: "", + IncludeDeleted: false, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{}, bots) + }) + + t.Run("get bots, page=0, perPage=10, include deleted", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 10, + OwnerId: "", + IncludeDeleted: true, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{bot1, deletedBot1, bot2, bot3, bot4, deletedBot2}, bots) + }) + + t.Run("get bots, page=0, perPage=1, include deleted", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 1, + OwnerId: "", + IncludeDeleted: true, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{bot1}, bots) + }) + + t.Run("get bots, page=1, perPage=2, include deleted", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 1, + PerPage: 2, + OwnerId: "", + IncludeDeleted: true, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{bot2, bot3}, bots) + }) + + t.Run("get bots, page=2, perPage=2, include deleted", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 2, + PerPage: 2, + OwnerId: "", + IncludeDeleted: true, + }) + require.Nil(t, err) + assert.Equal(t, model.BotList{bot4, deletedBot2}, bots) + }) + + t.Run("get offset=0, limit=10, creator id 1", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 10, + OwnerId: OwnerId1, + IncludeDeleted: false, + }) + require.Nil(t, err) + require.Equal(t, model.BotList{bot1, bot2, bot3}, bots) + }) + + t.Run("get offset=0, limit=10, creator id 2", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 10, + OwnerId: OwnerId2, + IncludeDeleted: false, + }) + require.Nil(t, err) + require.Equal(t, model.BotList{bot4}, bots) + }) + + t.Run("get offset=0, limit=10, include deleted, creator id 1", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 10, + OwnerId: OwnerId1, + IncludeDeleted: true, + }) + require.Nil(t, err) + require.Equal(t, model.BotList{bot1, deletedBot1, bot2, bot3}, bots) + }) + + t.Run("get offset=0, limit=10, include deleted, creator id 2", func(t *testing.T) { + bots, err := th.App.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 10, + OwnerId: OwnerId2, + IncludeDeleted: true, + }) + require.Nil(t, err) + require.Equal(t, model.BotList{bot4, deletedBot2}, bots) + }) +} + +func TestUpdateBotActive(t *testing.T) { + t.Run("unknown bot", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + _, err := th.App.UpdateBotActive(model.NewId(), false) + require.NotNil(t, err) + require.Equal(t, "store.sql_user.missing_account.const", err.Id) + }) + + t.Run("disable/enable bot", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot.UserId) + + disabledBot, err := th.App.UpdateBotActive(bot.UserId, false) + require.Nil(t, err) + require.NotEqual(t, 0, disabledBot.DeleteAt) + + // Disabling should be idempotent + disabledBotAgain, err := th.App.UpdateBotActive(bot.UserId, false) + require.Nil(t, err) + require.Equal(t, disabledBot.DeleteAt, disabledBotAgain.DeleteAt) + + reenabledBot, err := th.App.UpdateBotActive(bot.UserId, true) + require.Nil(t, err) + require.EqualValues(t, 0, reenabledBot.DeleteAt) + + // Re-enabling should be idempotent + reenabledBotAgain, err := th.App.UpdateBotActive(bot.UserId, true) + require.Nil(t, err) + require.Equal(t, reenabledBot.DeleteAt, reenabledBotAgain.DeleteAt) + }) +} + +func TestPermanentDeleteBot(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + + require.Nil(t, th.App.PermanentDeleteBot(bot.UserId)) + + _, err = th.App.GetBot(bot.UserId, false) + require.NotNil(t, err) + require.Equal(t, "store.sql_bot.get.missing.app_error", err.Id) +} + +func TestDisableUserBots(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + ownerId1 := model.NewId() + ownerId2 := model.NewId() + + bots := []*model.Bot{} + defer func() { + for _, bot := range bots { + th.App.PermanentDeleteBot(bot.UserId) + } + }() + + for i := 0; i < 46; i++ { + bot, err := th.App.CreateBot(&model.Bot{ + Username: fmt.Sprintf("username%v", i), + Description: "a bot", + OwnerId: ownerId1, + }) + require.Nil(t, err) + bots = append(bots, bot) + } + require.Len(t, bots, 46) + + u2bot1, err := th.App.CreateBot(&model.Bot{ + Username: "username_nodisable", + Description: "a bot", + OwnerId: ownerId2, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(u2bot1.UserId) + + err = th.App.disableUserBots(ownerId1) + require.Nil(t, err) + + // Check all bots and corrensponding users are disabled for creator 1 + for _, bot := range bots { + retbot, err2 := th.App.GetBot(bot.UserId, true) + require.Nil(t, err2) + require.NotZero(t, retbot.DeleteAt, bot.Username) + } + + // Check bots and corresponding user not disabled for creator 2 + bot, err := th.App.GetBot(u2bot1.UserId, true) + require.Nil(t, err) + require.Zero(t, bot.DeleteAt) + + user, err := th.App.GetUser(u2bot1.UserId) + require.Nil(t, err) + require.Zero(t, user.DeleteAt) + + // Bad id doesn't do anything or break horribly + err = th.App.disableUserBots(model.NewId()) + require.Nil(t, err) +} + +func sToP(s string) *string { + return &s +} diff --git a/app/diagnostics.go b/app/diagnostics.go index f4cdf6cb01..a0d57ad946 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -122,6 +122,7 @@ func pluginActivated(pluginStates map[string]*model.PluginState, pluginId string func (a *App) trackActivity() { var userCount int64 + var botAccountsCount int64 var activeUsersDailyCount int64 var activeUsersMonthlyCount int64 var inactiveUserCount int64 @@ -147,10 +148,19 @@ func (a *App) trackActivity() { activeUsersMonthlyCount = r.Data.(int64) } - if ucr := <-a.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + if ucr := <-a.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + }); ucr.Err == nil { userCount = ucr.Data.(int64) } + if bc := <-a.Srv.Store.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + ExcludeRegularUsers: true, + }); bc.Err == nil { + botAccountsCount = bc.Data.(int64) + } + if iucr := <-a.Srv.Store.User().AnalyticsGetInactiveUsersCount(); iucr.Err == nil { inactiveUserCount = iucr.Data.(int64) } @@ -197,6 +207,7 @@ func (a *App) trackActivity() { a.SendDiagnostic(TRACK_ACTIVITY, map[string]interface{}{ "registered_users": userCount, + "bot_accounts": botAccountsCount, "active_users_daily": activeUsersDailyCount, "active_users_monthly": activeUsersMonthlyCount, "registered_deactivated_users": inactiveUserCount, @@ -281,6 +292,7 @@ func (a *App) trackConfig() { "enable_email_invitations": *cfg.ServiceSettings.EnableEmailInvitations, "experimental_channel_organization": *cfg.ServiceSettings.ExperimentalChannelOrganization, "experimental_ldap_group_sync": *cfg.ServiceSettings.ExperimentalLdapGroupSync, + "disable_bots_when_owner_is_deactivated": *cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated, }) a.SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{ diff --git a/app/import_functions_test.go b/app/import_functions_test.go index 248940367e..df9f611e4c 100644 --- a/app/import_functions_test.go +++ b/app/import_functions_test.go @@ -553,7 +553,10 @@ func TestImportImportUser(t *testing.T) { // Check how many users are in the database. var userCount int64 - if r := <-th.App.Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r := <-th.App.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + IncludeBotAccounts: false, + }); r.Err == nil { userCount = r.Data.(int64) } else { t.Fatalf("Failed to get user count.") @@ -568,7 +571,10 @@ func TestImportImportUser(t *testing.T) { } // Check that no more users are in the DB. - if r := <-th.App.Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r := <-th.App.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + IncludeBotAccounts: false, + }); r.Err == nil { if r.Data.(int64) != userCount { t.Fatalf("Unexpected number of users") } @@ -586,7 +592,10 @@ func TestImportImportUser(t *testing.T) { } // Check that no more users are in the DB. - if r := <-th.App.Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r := <-th.App.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + IncludeBotAccounts: false, + }); r.Err == nil { if r.Data.(int64) != userCount { t.Fatalf("Unexpected number of users") } @@ -603,7 +612,10 @@ func TestImportImportUser(t *testing.T) { } // Check that no more users are in the DB. - if r := <-th.App.Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r := <-th.App.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + IncludeBotAccounts: false, + }); r.Err == nil { if r.Data.(int64) != userCount { t.Fatalf("Unexpected number of users") } @@ -628,7 +640,10 @@ func TestImportImportUser(t *testing.T) { } // Check that one more user is in the DB. - if r := <-th.App.Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r := <-th.App.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + IncludeBotAccounts: false, + }); r.Err == nil { if r.Data.(int64) != userCount+1 { t.Fatalf("Unexpected number of users") } @@ -685,7 +700,10 @@ func TestImportImportUser(t *testing.T) { } // Check user count the same. - if r := <-th.App.Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r := <-th.App.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + IncludeBotAccounts: false, + }); r.Err == nil { if r.Data.(int64) != userCount+1 { t.Fatalf("Unexpected number of users") } diff --git a/app/license.go b/app/license.go index 34d9fd47d9..698c2194ba 100644 --- a/app/license.go +++ b/app/license.go @@ -52,7 +52,7 @@ func (a *App) SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) } license := model.LicenseFromJson(strings.NewReader(licenseStr)) - result := <-a.Srv.Store.User().AnalyticsUniqueUserCount("") + result := <-a.Srv.Store.User().Count(model.UserCountOptions{}) if result.Err != nil { return nil, model.NewAppError("addLicense", "api.license.add_license.invalid_count.app_error", nil, result.Err.Error(), http.StatusBadRequest) } diff --git a/app/login.go b/app/login.go index a505f47bca..6c153d5395 100644 --- a/app/login.go +++ b/app/login.go @@ -57,6 +57,11 @@ func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, l // then trust the proxy and cert that the correct user is supplied and allow // them access if *a.Config().ExperimentalSettings.ClientSideCertEnable && *a.Config().ExperimentalSettings.ClientSideCertCheck == model.CLIENT_SIDE_CERT_CHECK_PRIMARY_AUTH { + // Unless the user is a bot. + if err = checkUserNotBot(user); err != nil { + return nil, err + } + return user, nil } diff --git a/app/oauth.go b/app/oauth.go index d5079d2844..9678ccc508 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -551,6 +551,13 @@ func (a *App) LoginByOAuth(service string, userData io.Reader, teamId string) (* return nil, err } } else { + // OAuth doesn't run through CheckUserPreflightAuthenticationCriteria, so prevent bot login + // here manually. Technically, the auth data above will fail to match a bot in the first + // place, but explicit is always better. + if user.IsBot { + return nil, model.NewAppError("loginByOAuth", "api.user.login_by_oauth.bot_login_forbidden.app_error", nil, "", http.StatusForbidden) + } + if err = a.UpdateOAuthUserAttrs(bytes.NewReader(buf.Bytes()), user, provider, service); err != nil { return nil, err } diff --git a/app/plugin_api.go b/app/plugin_api.go index bf3a1c4f44..80587c0a58 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -686,3 +686,35 @@ func (api *PluginAPI) LogError(msg string, keyValuePairs ...interface{}) { func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...interface{}) { api.logger.Warn(msg, keyValuePairs...) } + +func (api *PluginAPI) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) { + // Bots created by a plugin should use the plugin's ID for the creator field, unless + // otherwise specified by the plugin. + if bot.OwnerId == "" { + bot.OwnerId = api.id + } + + return api.app.CreateBot(bot) +} + +func (api *PluginAPI) PatchBot(userId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) { + return api.app.PatchBot(userId, botPatch) +} + +func (api *PluginAPI) GetBot(userId string, includeDeleted bool) (*model.Bot, *model.AppError) { + return api.app.GetBot(userId, includeDeleted) +} + +func (api *PluginAPI) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) { + bots, err := api.app.GetBots(options) + + return []*model.Bot(bots), err +} + +func (api *PluginAPI) UpdateBotActive(userId string, active bool) (*model.Bot, *model.AppError) { + return api.app.UpdateBotActive(userId, active) +} + +func (api *PluginAPI) PermanentDeleteBot(userId string) *model.AppError { + return api.app.PermanentDeleteBot(userId) +} diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 73355c3fb0..55dc1b660f 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -882,6 +882,151 @@ func TestPluginAPI_SearchTeams(t *testing.T) { }) } +func TestPluginBots(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + setupPluginApiTest(t, + ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) { + createdBot, err := p.API.CreateBot(&model.Bot{ + Username: "bot", + Description: "a plugin bot", + }) + if err != nil { + return nil, err.Error() + "failed to create bot" + } + + fetchedBot, err := p.API.GetBot(createdBot.UserId, false) + if err != nil { + return nil, err.Error() + "failed to get bot" + } + if fetchedBot.Description != "a plugin bot" { + return nil, "GetBot did not return the expected bot Description" + } + if fetchedBot.OwnerId != "testpluginbots" { + return nil, "GetBot did not return the expected bot OwnerId" + } + + updatedDescription := createdBot.Description + ", updated" + patchedBot, err := p.API.PatchBot(createdBot.UserId, &model.BotPatch{ + Description: &updatedDescription, + }) + if err != nil { + return nil, err.Error() + "failed to patch bot" + } + + fetchedBot, err = p.API.GetBot(patchedBot.UserId, false) + if err != nil { + return nil, err.Error() + "failed to get bot" + } + + if fetchedBot.UserId != patchedBot.UserId { + return nil, "GetBot did not return the expected bot" + } + if fetchedBot.Description != "a plugin bot, updated" { + return nil, "GetBot did not return the updated bot Description" + } + + fetchedBots, err := p.API.GetBots(&model.BotGetOptions{ + Page: 0, + PerPage: 1, + OwnerId: "", + IncludeDeleted: false, + }) + if err != nil { + return nil, err.Error() + "failed to get bots" + } + + if len(fetchedBots) != 1 { + return nil, "GetBots did not return a single bot" + } + if fetchedBot.UserId != fetchedBots[0].UserId { + return nil, "GetBots did not return the expected bot" + } + + _, err = p.API.UpdateBotActive(fetchedBot.UserId, false) + if err != nil { + return nil, err.Error() + "failed to disable bot" + } + + fetchedBot, err = p.API.GetBot(patchedBot.UserId, false) + if err == nil { + return nil, "expected not to find disabled bot" + } + + _, err = p.API.UpdateBotActive(fetchedBot.UserId, true) + if err != nil { + return nil, err.Error() + "failed to disable bot" + } + + fetchedBot, err = p.API.GetBot(patchedBot.UserId, false) + if err != nil { + return nil, err.Error() + "failed to get bot after enabling" + } + if fetchedBot.UserId != patchedBot.UserId { + return nil, "GetBot did not return the expected bot after enabling" + } + + err = p.API.PermanentDeleteBot(patchedBot.UserId) + if err != nil { + return nil, err.Error() + "failed to delete bot" + } + + _, err = p.API.GetBot(patchedBot.UserId, false) + if err == nil { + return nil, err.Error() + "found bot after permanently deleting" + } + + createdBotWithOverriddenCreator, err := p.API.CreateBot(&model.Bot{ + Username: "bot", + Description: "a plugin bot", + OwnerId: "abc123", + }) + if err != nil { + return nil, err.Error() + "failed to create bot with overridden creator" + } + + fetchedBot, err = p.API.GetBot(createdBotWithOverriddenCreator.UserId, false) + if err != nil { + return nil, err.Error() + "failed to get bot" + } + if fetchedBot.Description != "a plugin bot" { + return nil, "GetBot did not return the expected bot Description" + } + if fetchedBot.OwnerId != "abc123" { + return nil, "GetBot did not return the expected bot OwnerId" + } + + return nil, "" + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `, + `{"id": "testpluginbots", "backend": {"executable": "backend.exe"}}`, + "testpluginbots", + th.App, + ) + + hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin("testpluginbots") + assert.NoError(t, err) + _, errString := hooks.MessageWillBePosted(nil, nil) + assert.Empty(t, errString) +} + func TestPluginAPI_GetTeamMembersForUser(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/security_update_check.go b/app/security_update_check.go index 2356eb4b1a..fb3bfcc860 100644 --- a/app/security_update_check.go +++ b/app/security_update_check.go @@ -63,7 +63,9 @@ func (s *Server) DoSecurityUpdateCheck() { <-s.Store.System().Update(systemSecurityLastTime) } - if ucr := <-s.Store.User().GetTotalUsersCount(); ucr.Err == nil { + if ucr := <-s.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + }); ucr.Err == nil { v.Set(PROP_SECURITY_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) } diff --git a/app/user.go b/app/user.go index df442779e4..ee9e385c10 100644 --- a/app/user.go +++ b/app/user.go @@ -171,7 +171,9 @@ func (a *App) IsUserSignUpAllowed() *model.AppError { func (a *App) IsFirstUserAccount() bool { if a.SessionCacheLength() == 0 { - cr := <-a.Srv.Store.User().GetTotalUsersCount() + cr := <-a.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + }) if cr.Err != nil { mlog.Error(fmt.Sprint(cr.Err)) return false @@ -195,7 +197,9 @@ func (a *App) CreateUser(user *model.User) (*model.User, *model.AppError) { // Below is a special case where the first user in the entire // system is granted the system_admin role - result := <-a.Srv.Store.User().GetTotalUsersCount() + result := <-a.Srv.Store.User().Count(model.UserCountOptions{ + IncludeDeleted: true, + }) if result.Err != nil { return nil, result.Err } @@ -867,6 +871,40 @@ func (a *App) UpdatePasswordAsUser(userId, currentPassword, newPassword string) return a.UpdatePasswordSendEmail(user, newPassword, T("api.user.update_password.menu")) } +func (a *App) userDeactivated(user *model.User) *model.AppError { + if err := a.RevokeAllSessions(user.Id); err != nil { + return err + } + + a.SetStatusOffline(user.Id, false) + + if *a.Config().ServiceSettings.DisableBotsWhenOwnerIsDeactivated { + a.disableUserBots(user.Id) + } + + return nil +} + +func (a *App) invalidateUserChannelMembersCaches(user *model.User) *model.AppError { + teamsForUser, err := a.GetTeamsForUser(user.Id) + if err != nil { + return err + } + + for _, team := range teamsForUser { + channelsForUser, err := a.GetChannelsForUser(team.Id, user.Id, false) + if err != nil { + return err + } + + for _, channel := range *channelsForUser { + a.InvalidateCacheForChannelMembers(channel.Id) + } + } + + return nil +} + func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.AppError) { if active { user.DeleteAt = 0 @@ -878,35 +916,16 @@ func (a *App) UpdateActive(user *model.User, active bool) (*model.User, *model.A if result.Err != nil { return nil, result.Err } - - if user.DeleteAt > 0 { - if err := a.RevokeAllSessions(user.Id); err != nil { - return nil, err - } - } - ruser := result.Data.([2]*model.User)[0] if !active { - a.SetStatusOffline(ruser.Id, false) - } - - teamsForUser, err := a.GetTeamsForUser(user.Id) - if err != nil { - return nil, err - } - - for _, team := range teamsForUser { - channelsForUser, err := a.GetChannelsForUser(team.Id, user.Id, false) - if err != nil { + if err := a.userDeactivated(ruser); err != nil { return nil, err } - - for _, channel := range *channelsForUser { - a.InvalidateCacheForChannelMembers(channel.Id) - } } + a.invalidateUserChannelMembersCaches(user) + a.sendUpdatedUserEvent(*ruser) return ruser, nil @@ -1500,8 +1519,11 @@ func (a *App) GetVerifyEmailToken(token string) (*model.Token, *model.AppError) return rtoken, nil } +// GetTotalUsersStats is used for the DM list total func (a *App) GetTotalUsersStats() (*model.UsersStats, *model.AppError) { - result := <-a.Srv.Store.User().GetTotalUsersCount() + result := <-a.Srv.Store.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + }) if result.Err != nil { return nil, result.Err } diff --git a/app/user_test.go b/app/user_test.go index 03061e79ab..ea7801dc35 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -172,6 +172,51 @@ func TestUpdateUserActive(t *testing.T) { assert.Nil(t, err) } +func TestUpdateActiveBotsSideEffect(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "username", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + defer th.App.PermanentDeleteBot(bot.UserId) + + // Automatic deactivation disabled + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated = false + }) + + th.App.UpdateActive(th.BasicUser, false) + + retbot1, err := th.App.GetBot(bot.UserId, true) + require.Nil(t, err) + require.Zero(t, retbot1.DeleteAt) + user1, err := th.App.GetUser(bot.UserId) + require.Nil(t, err) + require.Zero(t, user1.DeleteAt) + + th.App.UpdateActive(th.BasicUser, true) + + // Automatic deactivation enabled + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.DisableBotsWhenOwnerIsDeactivated = true + }) + + th.App.UpdateActive(th.BasicUser, false) + + retbot2, err := th.App.GetBot(bot.UserId, true) + require.Nil(t, err) + require.NotZero(t, retbot2.DeleteAt) + user2, err := th.App.GetUser(bot.UserId) + require.Nil(t, err) + require.NotZero(t, user2.DeleteAt) + + th.App.UpdateActive(th.BasicUser, true) +} + func TestUpdateOAuthUserAttrs(t *testing.T) { th := Setup(t) defer th.TearDown() @@ -292,7 +337,7 @@ func TestUpdateUserEmail(t *testing.T) { user := th.CreateUser() - t.Run("RequireVerification", func(t *testing.T){ + t.Run("RequireVerification", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.RequireEmailVerification = true }) @@ -318,7 +363,7 @@ func TestUpdateUserEmail(t *testing.T) { assert.True(t, user2.EmailVerified) }) - t.Run("RequireVerificationAlreadyUsedEmail", func(t *testing.T){ + t.Run("RequireVerificationAlreadyUsedEmail", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.RequireEmailVerification = true }) @@ -332,7 +377,7 @@ func TestUpdateUserEmail(t *testing.T) { assert.Nil(t, user3) }) - t.Run("NoVerification", func(t *testing.T){ + t.Run("NoVerification", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.RequireEmailVerification = false }) @@ -648,7 +693,7 @@ func TestPasswordRecovery(t *testing.T) { token, err = th.App.CreatePasswordRecoveryToken(th.BasicUser.Id, th.BasicUser.Email) assert.Nil(t, err) - th.App.UpdateConfig(func (c *model.Config){ + th.App.UpdateConfig(func(c *model.Config) { *c.EmailSettings.RequireEmailVerification = false }) @@ -659,4 +704,3 @@ func TestPasswordRecovery(t *testing.T) { err = th.App.ResetPasswordFromToken(token.Token, "abcdefgh") assert.NotNil(t, err) } - diff --git a/i18n/en.json b/i18n/en.json index 4468d138d6..51bf9f6baa 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2258,6 +2258,10 @@ "id": "api.user.ldap_to_email.not_ldap_account.app_error", "translation": "This user account does not use AD/LDAP" }, + { + "id": "api.user.login.bot_login_forbidden.app_error", + "translation": "Bot login is forbidden" + }, { "id": "api.user.login.blank_pwd.app_error", "translation": "Password field must not be blank" @@ -2286,6 +2290,10 @@ "id": "api.user.login.use_auth_service.app_error", "translation": "Please sign in using {{.AuthService}}" }, + { + "id": "api.user.login_by_oauth.bot_login_forbidden.app_error", + "translation": "Bot login is forbidden" + }, { "id": "api.user.login_by_oauth.not_available.app_error", "translation": "{{.Service}} SSO through OAuth 2.0 not available on this server" @@ -3830,6 +3838,30 @@ "id": "model.authorize.is_valid.user_id.app_error", "translation": "Invalid user id" }, + { + "id": "model.bot.is_valid.create_at.app_error", + "translation": "Invalid create at" + }, + { + "id": "model.bot.is_valid.creator_id.app_error", + "translation": "Invalid creator id" + }, + { + "id": "model.bot.is_valid.description.app_error", + "translation": "Invalid description" + }, + { + "id": "model.bot.is_valid.update_at.app_error", + "translation": "Invalid update at" + }, + { + "id": "model.bot.is_valid.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.bot.is_valid.username.app_error", + "translation": "Invalid username" + }, { "id": "model.channel.is_valid.2_or_more.app_error", "translation": "Name must be 2 or more lowercase alphanumeric characters" @@ -5026,6 +5058,34 @@ "id": "store.sql_audit.save.saving.app_error", "translation": "We encountered an error saving the audit" }, + { + "id": "store.sql_bot.delete.app_error", + "translation": "Unable to delete the bot" + }, + { + "id": "store.sql_bot.get.app_error", + "translation": "Unable to get the bot" + }, + { + "id": "store.sql_bot.get.missing.app_error", + "translation": "Bot does not exist" + }, + { + "id": "store.sql_bot.get_all.app_error", + "translation": "Unable to get the bots" + }, + { + "id": "store.sql_bot.save.app_error", + "translation": "Unable to save the bot" + }, + { + "id": "store.sql_bot.update.app_error", + "translation": "Unable to update the bot" + }, + { + "id": "store.sql_bot.update.updating.app_error", + "translation": "We encountered an error updating the bot" + }, { "id": "store.sql_channel.analytics_deleted_type_count.app_error", "translation": "Unable to get deleted channel type counts" diff --git a/model/bot.go b/model/bot.go new file mode 100644 index 0000000000..84792e0aa5 --- /dev/null +++ b/model/bot.go @@ -0,0 +1,209 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "unicode/utf8" +) + +const ( + BOT_DISPLAY_NAME_MAX_RUNES = USER_FIRST_NAME_MAX_RUNES + BOT_DESCRIPTION_MAX_RUNES = 1024 + BOT_CREATOR_ID_MAX_RUNES = KEY_VALUE_PLUGIN_ID_MAX_RUNES // UserId or PluginId +) + +// Bot is a special type of User meant for programmatic interactions. +// Note that the primary key of a bot is the UserId, and matches the primary key of the +// corresponding user. +type Bot struct { + UserId string `json:"user_id"` + Username string `json:"username"` + DisplayName string `json:"display_name,omitempty"` + Description string `json:"description,omitempty"` + OwnerId string `json:"creator_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` +} + +// BotPatch is a description of what fields to update on an existing bot. +type BotPatch struct { + Username *string `json:"username"` + DisplayName *string `json:"display_name"` + Description *string `json:"description"` +} + +// BotGetOptions acts as a filter on bulk bot fetching queries. +type BotGetOptions struct { + OwnerId string + IncludeDeleted bool + OnlyOrphaned bool + Page int + PerPage int +} + +// BotList is a list of bots. +type BotList []*Bot + +// Trace describes the minimum information required to identify a bot for the purpose of logging. +func (b *Bot) Trace() map[string]interface{} { + return map[string]interface{}{"user_id": b.UserId} +} + +// Clone returns a shallow copy of the bot. +func (b *Bot) Clone() *Bot { + copy := *b + return © +} + +// IsValid validates the bot and returns an error if it isn't configured correctly. +func (b *Bot) IsValid() *AppError { + if !IsValidId(b.UserId) { + return NewAppError("Bot.IsValid", "model.bot.is_valid.user_id.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if !IsValidUsername(b.Username) { + return NewAppError("Bot.IsValid", "model.bot.is_valid.username.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if utf8.RuneCountInString(b.DisplayName) > BOT_DISPLAY_NAME_MAX_RUNES { + return NewAppError("Bot.IsValid", "model.bot.is_valid.user_id.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if utf8.RuneCountInString(b.Description) > BOT_DESCRIPTION_MAX_RUNES { + return NewAppError("Bot.IsValid", "model.bot.is_valid.description.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if len(b.OwnerId) == 0 || utf8.RuneCountInString(b.OwnerId) > BOT_CREATOR_ID_MAX_RUNES { + return NewAppError("Bot.IsValid", "model.bot.is_valid.creator_id.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if b.CreateAt == 0 { + return NewAppError("Bot.IsValid", "model.bot.is_valid.create_at.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if b.UpdateAt == 0 { + return NewAppError("Bot.IsValid", "model.bot.is_valid.update_at.app_error", b.Trace(), "", http.StatusBadRequest) + } + + return nil +} + +// PreSave should be run before saving a new bot to the database. +func (b *Bot) PreSave() { + b.CreateAt = GetMillis() + b.UpdateAt = b.CreateAt + b.DeleteAt = 0 +} + +// PreUpdate should be run before saving an updated bot to the database. +func (b *Bot) PreUpdate() { + b.UpdateAt = GetMillis() +} + +// Etag generates an etag for caching. +func (b *Bot) Etag() string { + return Etag(b.UserId, b.UpdateAt) +} + +// ToJson serializes the bot to json. +func (b *Bot) ToJson() []byte { + data, _ := json.Marshal(b) + return data +} + +// BotFromJson deserializes a bot from json. +func BotFromJson(data io.Reader) *Bot { + var bot *Bot + json.NewDecoder(data).Decode(&bot) + return bot +} + +// Patch modifies an existing bot with optional fields from the given patch. +func (b *Bot) Patch(patch *BotPatch) { + if patch.Username != nil { + b.Username = *patch.Username + } + + if patch.DisplayName != nil { + b.DisplayName = *patch.DisplayName + } + + if patch.Description != nil { + b.Description = *patch.Description + } +} + +// ToJson serializes the bot patch to json. +func (b *BotPatch) ToJson() []byte { + data, err := json.Marshal(b) + if err != nil { + return nil + } + + return data +} + +// BotPatchFromJson deserializes a bot patch from json. +func BotPatchFromJson(data io.Reader) *BotPatch { + decoder := json.NewDecoder(data) + var botPatch BotPatch + err := decoder.Decode(&botPatch) + if err != nil { + return nil + } + + return &botPatch +} + +// UserFromBot returns a user model describing the bot fields stored in the User store. +func UserFromBot(b *Bot) *User { + return &User{ + Id: b.UserId, + Username: b.Username, + Email: fmt.Sprintf("%s@localhost", strings.ToLower(b.Username)), + FirstName: b.DisplayName, + } +} + +// BotListFromJson deserializes a list of bots from json. +func BotListFromJson(data io.Reader) BotList { + var bots BotList + json.NewDecoder(data).Decode(&bots) + return bots +} + +// ToJson serializes a list of bots to json. +func (l *BotList) ToJson() []byte { + b, _ := json.Marshal(l) + return b +} + +// Etag computes the etag for a list of bots. +func (l *BotList) Etag() string { + id := "0" + var t int64 = 0 + var delta int64 = 0 + + for _, v := range *l { + if v.UpdateAt > t { + t = v.UpdateAt + id = v.UserId + } + + } + + return Etag(id, t, delta, len(*l)) +} + +// MakeBotNotFoundError creates the error returned when a bot does not exist, or when the user isn't allowed to query the bot. +// The errors must the same in both cases to avoid leaking that a user is a bot. +func MakeBotNotFoundError(userId string) *AppError { + return NewAppError("SqlBotStore.Get", "store.sql_bot.get.missing.app_error", map[string]interface{}{"user_id": userId}, "", http.StatusNotFound) +} diff --git a/model/bot_test.go b/model/bot_test.go new file mode 100644 index 0000000000..4d49b1d339 --- /dev/null +++ b/model/bot_test.go @@ -0,0 +1,666 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBotTrace(t *testing.T) { + bot := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + + require.Equal(t, map[string]interface{}{"user_id": bot.UserId}, bot.Trace()) +} + +func TestBotClone(t *testing.T) { + bot := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + + clone := bot.Clone() + + require.Equal(t, bot, bot.Clone()) + require.False(t, bot == clone) +} + +func TestBotIsValid(t *testing.T) { + testCases := []struct { + Description string + Bot *Bot + ExpectedIsValid bool + }{ + { + "nil bot", + &Bot{}, + false, + }, + { + "bot with missing user id", + &Bot{ + UserId: "", + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + false, + }, + { + "bot with invalid user id", + &Bot{ + UserId: "invalid", + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + false, + }, + { + "bot with missing username", + &Bot{ + UserId: NewId(), + Username: "", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + false, + }, + { + "bot with invalid username", + &Bot{ + UserId: NewId(), + Username: "a@", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + false, + }, + { + "bot with long description", + &Bot{ + UserId: "", + Username: "username", + DisplayName: "display name", + Description: strings.Repeat("x", 1025), + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + false, + }, + { + "bot with missing creator id", + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: "", + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + false, + }, + { + "bot without create at timestamp", + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 0, + UpdateAt: 2, + DeleteAt: 3, + }, + false, + }, + { + "bot without update at timestamp", + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 0, + DeleteAt: 3, + }, + false, + }, + { + "bot", + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 0, + }, + true, + }, + { + "bot without description", + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 0, + }, + true, + }, + { + "deleted bot", + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "a description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + if testCase.ExpectedIsValid { + require.Nil(t, testCase.Bot.IsValid()) + } else { + require.NotNil(t, testCase.Bot.IsValid()) + } + }) + } +} + +func TestBotPreSave(t *testing.T) { + bot := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + DeleteAt: 0, + } + + originalBot := &*bot + + bot.PreSave() + assert.NotEqual(t, 0, bot.CreateAt) + assert.NotEqual(t, 0, bot.UpdateAt) + + originalBot.CreateAt = bot.CreateAt + originalBot.UpdateAt = bot.UpdateAt + assert.Equal(t, originalBot, bot) +} + +func TestBotPreUpdate(t *testing.T) { + bot := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + DeleteAt: 0, + } + + originalBot := &*bot + + bot.PreSave() + assert.NotEqual(t, 0, bot.UpdateAt) + + originalBot.UpdateAt = bot.UpdateAt + assert.Equal(t, originalBot, bot) +} + +func TestBotEtag(t *testing.T) { + t.Run("same etags", func(t *testing.T) { + bot1 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + bot2 := bot1 + + assert.Equal(t, bot1.Etag(), bot2.Etag()) + }) + t.Run("different etags", func(t *testing.T) { + t.Run("different user id", func(t *testing.T) { + bot1 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + bot2 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: bot1.OwnerId, + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + + assert.NotEqual(t, bot1.Etag(), bot2.Etag()) + }) + t.Run("different update at", func(t *testing.T) { + bot1 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + bot2 := &Bot{ + UserId: bot1.UserId, + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: bot1.OwnerId, + CreateAt: 1, + UpdateAt: 10, + DeleteAt: 3, + } + + assert.NotEqual(t, bot1.Etag(), bot2.Etag()) + }) + }) +} + +func TestBotToAndFromJson(t *testing.T) { + bot1 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + + bot2 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description 2", + OwnerId: NewId(), + CreateAt: 4, + UpdateAt: 5, + DeleteAt: 6, + } + + assert.Equal(t, bot1, BotFromJson(bytes.NewReader(bot1.ToJson()))) + assert.Equal(t, bot2, BotFromJson(bytes.NewReader(bot2.ToJson()))) +} + +func sToP(s string) *string { + return &s +} + +func TestBotPatch(t *testing.T) { + userId1 := NewId() + creatorId1 := NewId() + + testCases := []struct { + Description string + Bot *Bot + BotPatch *BotPatch + ExpectedBot *Bot + }{ + { + "no update", + &Bot{ + UserId: userId1, + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: creatorId1, + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + &BotPatch{}, + &Bot{ + UserId: userId1, + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: creatorId1, + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + }, + { + "partial update", + &Bot{ + UserId: userId1, + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: creatorId1, + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + &BotPatch{ + Username: sToP("new_username"), + DisplayName: nil, + Description: sToP("new description"), + }, + &Bot{ + UserId: userId1, + Username: "new_username", + DisplayName: "display name", + Description: "new description", + OwnerId: creatorId1, + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + }, + { + "full update", + &Bot{ + UserId: userId1, + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: creatorId1, + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + &BotPatch{ + Username: sToP("new_username"), + DisplayName: sToP("new display name"), + Description: sToP("new description"), + }, + &Bot{ + UserId: userId1, + Username: "new_username", + DisplayName: "new display name", + Description: "new description", + OwnerId: creatorId1, + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + testCase.Bot.Patch(testCase.BotPatch) + assert.Equal(t, testCase.ExpectedBot, testCase.Bot) + }) + } +} + +func TestBotPatchToAndFromJson(t *testing.T) { + botPatch1 := &BotPatch{ + Username: sToP("username"), + DisplayName: sToP("display name"), + Description: sToP("description"), + } + + botPatch2 := &BotPatch{ + Username: sToP("username"), + DisplayName: sToP("display name"), + Description: sToP("description 2"), + } + + assert.Equal(t, botPatch1, BotPatchFromJson(bytes.NewReader(botPatch1.ToJson()))) + assert.Equal(t, botPatch2, BotPatchFromJson(bytes.NewReader(botPatch2.ToJson()))) +} + +func TestUserFromBot(t *testing.T) { + bot1 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + + bot2 := &Bot{ + UserId: NewId(), + Username: "username2", + DisplayName: "display name 2", + Description: "description 2", + OwnerId: NewId(), + CreateAt: 4, + UpdateAt: 5, + DeleteAt: 6, + } + + assert.Equal(t, &User{ + Id: bot1.UserId, + Username: "username", + Email: "username@localhost", + FirstName: "display name", + }, UserFromBot(bot1)) + assert.Equal(t, &User{ + Id: bot2.UserId, + Username: "username2", + Email: "username2@localhost", + FirstName: "display name 2", + }, UserFromBot(bot2)) +} + +func TestBotListToAndFromJson(t *testing.T) { + testCases := []struct { + Description string + BotList BotList + }{ + { + "empty list", + BotList{}, + }, + { + "single item", + BotList{ + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + }, + }, + { + "multiple items", + BotList{ + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + }, + + &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description 2", + OwnerId: NewId(), + CreateAt: 4, + UpdateAt: 5, + DeleteAt: 6, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + assert.Equal(t, testCase.BotList, BotListFromJson(bytes.NewReader(testCase.BotList.ToJson()))) + }) + } +} + +func TestBotListEtag(t *testing.T) { + bot1 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 2, + DeleteAt: 3, + } + + bot1Updated := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 1, + UpdateAt: 10, + DeleteAt: 3, + } + + bot2 := &Bot{ + UserId: NewId(), + Username: "username", + DisplayName: "display name", + Description: "description", + OwnerId: NewId(), + CreateAt: 4, + UpdateAt: 5, + DeleteAt: 6, + } + + testCases := []struct { + Description string + BotListA BotList + BotListB BotList + ExpectedEqual bool + }{ + { + "empty lists", + BotList{}, + BotList{}, + true, + }, + { + "single item, same list", + BotList{bot1}, + BotList{bot1}, + true, + }, + { + "single item, different update at", + BotList{bot1}, + BotList{bot1Updated}, + false, + }, + { + "single item vs. multiple items", + BotList{bot1}, + BotList{bot1, bot2}, + false, + }, + { + "multiple items, different update at", + BotList{bot1, bot2}, + BotList{bot1Updated, bot2}, + false, + }, + { + "multiple items, same list", + BotList{bot1, bot2}, + BotList{bot1, bot2}, + true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + if testCase.ExpectedEqual { + assert.Equal(t, testCase.BotListA.Etag(), testCase.BotListB.Etag()) + } else { + assert.NotEqual(t, testCase.BotListA.Etag(), testCase.BotListB.Etag()) + } + }) + } +} diff --git a/model/client4.go b/model/client4.go index 320d471d5a..2736058259 100644 --- a/model/client4.go +++ b/model/client4.go @@ -150,6 +150,14 @@ func (c *Client4) GetUserByEmailRoute(email string) string { return fmt.Sprintf(c.GetUsersRoute()+"/email/%v", email) } +func (c *Client4) GetBotsRoute() string { + return fmt.Sprintf("/bots") +} + +func (c *Client4) GetBotRoute(botUserId string) string { + return fmt.Sprintf("%s/%s", c.GetBotsRoute(), botUserId) +} + func (c *Client4) GetTeamsRoute() string { return fmt.Sprintf("/teams") } @@ -442,6 +450,10 @@ func (c *Client4) DoApiPut(url string, data string) (*http.Response, *AppError) return c.DoApiRequest(http.MethodPut, c.ApiUrl+url, data, "") } +func (c *Client4) doApiPutBytes(url string, data []byte) (*http.Response, *AppError) { + return c.doApiRequestBytes(http.MethodPut, c.ApiUrl+url, data, "") +} + func (c *Client4) DoApiDelete(url string) (*http.Response, *AppError) { return c.DoApiRequest(http.MethodDelete, c.ApiUrl+url, "", "") } @@ -1335,6 +1347,111 @@ func (c *Client4) EnableUserAccessToken(tokenId string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } +// Bots section + +// CreateBot creates a bot in the system based on the provided bot struct. +func (c *Client4) CreateBot(bot *Bot) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotsRoute(), bot.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// PatchBot partially updates a bot. Any missing fields are not updated. +func (c *Client4) PatchBot(userId string, patch *BotPatch) (*Bot, *Response) { + r, err := c.doApiPutBytes(c.GetBotRoute(userId), patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// GetBot fetches the given, undeleted bot. +func (c *Client4) GetBot(userId string, etag string) (*Bot, *Response) { + r, err := c.DoApiGet(c.GetBotRoute(userId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// GetBot fetches the given bot, even if it is deleted. +func (c *Client4) GetBotIncludeDeleted(userId string, etag string) (*Bot, *Response) { + r, err := c.DoApiGet(c.GetBotRoute(userId)+"?include_deleted=true", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// GetBots fetches the given page of bots, excluding deleted. +func (c *Client4) GetBots(page, perPage int, etag string) ([]*Bot, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetBotsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotListFromJson(r.Body), BuildResponse(r) +} + +// GetBotsIncludeDeleted fetches the given page of bots, including deleted. +func (c *Client4) GetBotsIncludeDeleted(page, perPage int, etag string) ([]*Bot, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_deleted=true", page, perPage) + r, err := c.DoApiGet(c.GetBotsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotListFromJson(r.Body), BuildResponse(r) +} + +// GetBotsOrphaned fetches the given page of bots, only including orphanded bots. +func (c *Client4) GetBotsOrphaned(page, perPage int, etag string) ([]*Bot, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&only_orphaned=true", page, perPage) + r, err := c.DoApiGet(c.GetBotsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotListFromJson(r.Body), BuildResponse(r) +} + +// DisableBot disables the given bot in the system. +func (c *Client4) DisableBot(botUserId string) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/disable", nil) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// EnableBot disables the given bot in the system. +func (c *Client4) EnableBot(botUserId string) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/enable", nil) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// AssignBot assigns the given bot to the given user +func (c *Client4) AssignBot(botUserId, newOwnerId string) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/assign/"+newOwnerId, nil) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + // Team Section // CreateTeam creates a team in the system based on the provided team struct. diff --git a/model/config.go b/model/config.go index 1ae3c1d4f7..ae71805951 100644 --- a/model/config.go +++ b/model/config.go @@ -287,6 +287,7 @@ type ServiceSettings struct { ExperimentalStrictCSRFEnforcement *bool EnableEmailInvitations *bool ExperimentalLdapGroupSync *bool + DisableBotsWhenOwnerIsDeactivated *bool } func (s *ServiceSettings) SetDefaults() { @@ -621,6 +622,10 @@ func (s *ServiceSettings) SetDefaults() { if s.ExperimentalStrictCSRFEnforcement == nil { s.ExperimentalStrictCSRFEnforcement = NewBool(false) } + + if s.DisableBotsWhenOwnerIsDeactivated == nil { + s.DisableBotsWhenOwnerIsDeactivated = NewBool(true) + } } type ClusterSettings struct { diff --git a/model/permission.go b/model/permission.go index 737321cc71..1af25daad1 100644 --- a/model/permission.go +++ b/model/permission.go @@ -69,6 +69,11 @@ var PERMISSION_MANAGE_JOBS *Permission var PERMISSION_CREATE_USER_ACCESS_TOKEN *Permission var PERMISSION_READ_USER_ACCESS_TOKEN *Permission var PERMISSION_REVOKE_USER_ACCESS_TOKEN *Permission +var PERMISSION_CREATE_BOT *Permission +var PERMISSION_READ_BOTS *Permission +var PERMISSION_READ_OTHERS_BOTS *Permission +var PERMISSION_MANAGE_BOTS *Permission +var PERMISSION_MANAGE_OTHERS_BOTS *Permission // General permission that encompasses all system admin functions // in the future this could be broken up to allow access to some @@ -396,6 +401,36 @@ func initializePermissions() { "authentication.permissions.revoke_user_access_token.description", PERMISSION_SCOPE_SYSTEM, } + PERMISSION_CREATE_BOT = &Permission{ + "create_bot", + "authentication.permissions.create_bot.name", + "authentication.permissions.create_bot.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_READ_BOTS = &Permission{ + "read_bots", + "authentication.permissions.read_bots.name", + "authentication.permissions.read_bots.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_READ_OTHERS_BOTS = &Permission{ + "read_others_bots", + "authentication.permissions.read_others_bots.name", + "authentication.permissions.read_others_bots.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_BOTS = &Permission{ + "manage_bots", + "authentication.permissions.manage_bots.name", + "authentication.permissions.manage_bots.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_OTHERS_BOTS = &Permission{ + "manage_others_bots", + "authentication.permissions.manage_others_bots.name", + "authentication.permissions.manage_others_bots.description", + PERMISSION_SCOPE_SYSTEM, + } PERMISSION_MANAGE_JOBS = &Permission{ "manage_jobs", "authentication.permisssions.manage_jobs.name", @@ -457,6 +492,11 @@ func initializePermissions() { PERMISSION_CREATE_USER_ACCESS_TOKEN, PERMISSION_READ_USER_ACCESS_TOKEN, PERMISSION_REVOKE_USER_ACCESS_TOKEN, + PERMISSION_CREATE_BOT, + PERMISSION_READ_BOTS, + PERMISSION_READ_OTHERS_BOTS, + PERMISSION_MANAGE_BOTS, + PERMISSION_MANAGE_OTHERS_BOTS, PERMISSION_MANAGE_SYSTEM, } } diff --git a/model/role.go b/model/role.go index 27b32ed697..8fa7c81942 100644 --- a/model/role.go +++ b/model/role.go @@ -345,6 +345,11 @@ func MakeDefaultRoles() map[string]*Role { PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, PERMISSION_READ_USER_ACCESS_TOKEN.Id, PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + PERMISSION_CREATE_BOT.Id, + PERMISSION_READ_BOTS.Id, + PERMISSION_READ_OTHERS_BOTS.Id, + PERMISSION_MANAGE_BOTS.Id, + PERMISSION_MANAGE_OTHERS_BOTS.Id, PERMISSION_REMOVE_OTHERS_REACTIONS.Id, }, roles[TEAM_USER_ROLE_ID].Permissions..., diff --git a/model/user.go b/model/user.go index a400be40f5..36a62d9422 100644 --- a/model/user.go +++ b/model/user.go @@ -80,6 +80,7 @@ type User struct { MfaActive bool `json:"mfa_active,omitempty"` MfaSecret string `json:"mfa_secret,omitempty"` LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` + IsBot bool `db:"-" json:"is_bot,omitempty"` } type UserPatch struct { diff --git a/model/user_count.go b/model/user_count.go new file mode 100644 index 0000000000..cfe4f6e3f5 --- /dev/null +++ b/model/user_count.go @@ -0,0 +1,16 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +// Options for counting users +type UserCountOptions struct { + // Should include users that are bots + IncludeBotAccounts bool + // Should include deleted users (of any type) + IncludeDeleted bool + // Exclude regular users + ExcludeRegularUsers bool + // Only include users on a specific team. "" for any team. + TeamId string +} diff --git a/plugin/api.go b/plugin/api.go index 99cf91cc6f..a25ddd5470 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -508,6 +508,36 @@ type API interface { // // Minimum server version: 5.7 SendMail(to, subject, htmlBody string) *model.AppError + + // CreateBot creates the given bot and corresponding user. + // + // Minimum server version: 5.10 + CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) + + // PatchBot applies the given patch to the bot and corresponding user. + // + // Minimum server version: 5.10 + PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) + + // GetBot returns the given bot. + // + // Minimum server version: 5.10 + GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) + + // GetBots returns the requested page of bots. + // + // Minimum server version: 5.10 + GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) + + // UpdateBotActive marks a bot as active or inactive, along with its corresponding user. + // + // Minimum server version: 5.10 + UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) + + // PermanentDeleteBot permanently deletes a bot and its corresponding user. + // + // Minimum server version: 5.10 + PermanentDeleteBot(botUserId string) *model.AppError } var handshake = plugin.HandshakeConfig{ diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 71ae54f9c7..788baa9bd1 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -3783,3 +3783,179 @@ func (s *apiRPCServer) SendMail(args *Z_SendMailArgs, returns *Z_SendMailReturns } return nil } + +type Z_CreateBotArgs struct { + A *model.Bot +} + +type Z_CreateBotReturns struct { + A *model.Bot + B *model.AppError +} + +func (g *apiRPCClient) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) { + _args := &Z_CreateBotArgs{bot} + _returns := &Z_CreateBotReturns{} + if err := g.client.Call("Plugin.CreateBot", _args, _returns); err != nil { + log.Printf("RPC call to CreateBot API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) CreateBot(args *Z_CreateBotArgs, returns *Z_CreateBotReturns) error { + if hook, ok := s.impl.(interface { + CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) + }); ok { + returns.A, returns.B = hook.CreateBot(args.A) + } else { + return encodableError(fmt.Errorf("API CreateBot called but not implemented.")) + } + return nil +} + +type Z_PatchBotArgs struct { + A string + B *model.BotPatch +} + +type Z_PatchBotReturns struct { + A *model.Bot + B *model.AppError +} + +func (g *apiRPCClient) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) { + _args := &Z_PatchBotArgs{botUserId, botPatch} + _returns := &Z_PatchBotReturns{} + if err := g.client.Call("Plugin.PatchBot", _args, _returns); err != nil { + log.Printf("RPC call to PatchBot API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) PatchBot(args *Z_PatchBotArgs, returns *Z_PatchBotReturns) error { + if hook, ok := s.impl.(interface { + PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) + }); ok { + returns.A, returns.B = hook.PatchBot(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API PatchBot called but not implemented.")) + } + return nil +} + +type Z_GetBotArgs struct { + A string + B bool +} + +type Z_GetBotReturns struct { + A *model.Bot + B *model.AppError +} + +func (g *apiRPCClient) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) { + _args := &Z_GetBotArgs{botUserId, includeDeleted} + _returns := &Z_GetBotReturns{} + if err := g.client.Call("Plugin.GetBot", _args, _returns); err != nil { + log.Printf("RPC call to GetBot API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetBot(args *Z_GetBotArgs, returns *Z_GetBotReturns) error { + if hook, ok := s.impl.(interface { + GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetBot(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API GetBot called but not implemented.")) + } + return nil +} + +type Z_GetBotsArgs struct { + A *model.BotGetOptions +} + +type Z_GetBotsReturns struct { + A []*model.Bot + B *model.AppError +} + +func (g *apiRPCClient) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) { + _args := &Z_GetBotsArgs{options} + _returns := &Z_GetBotsReturns{} + if err := g.client.Call("Plugin.GetBots", _args, _returns); err != nil { + log.Printf("RPC call to GetBots API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetBots(args *Z_GetBotsArgs, returns *Z_GetBotsReturns) error { + if hook, ok := s.impl.(interface { + GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetBots(args.A) + } else { + return encodableError(fmt.Errorf("API GetBots called but not implemented.")) + } + return nil +} + +type Z_UpdateBotActiveArgs struct { + A string + B bool +} + +type Z_UpdateBotActiveReturns struct { + A *model.Bot + B *model.AppError +} + +func (g *apiRPCClient) UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) { + _args := &Z_UpdateBotActiveArgs{botUserId, active} + _returns := &Z_UpdateBotActiveReturns{} + if err := g.client.Call("Plugin.UpdateBotActive", _args, _returns); err != nil { + log.Printf("RPC call to UpdateBotActive API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) UpdateBotActive(args *Z_UpdateBotActiveArgs, returns *Z_UpdateBotActiveReturns) error { + if hook, ok := s.impl.(interface { + UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdateBotActive(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API UpdateBotActive called but not implemented.")) + } + return nil +} + +type Z_PermanentDeleteBotArgs struct { + A string +} + +type Z_PermanentDeleteBotReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) PermanentDeleteBot(botUserId string) *model.AppError { + _args := &Z_PermanentDeleteBotArgs{botUserId} + _returns := &Z_PermanentDeleteBotReturns{} + if err := g.client.Call("Plugin.PermanentDeleteBot", _args, _returns); err != nil { + log.Printf("RPC call to PermanentDeleteBot API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) PermanentDeleteBot(args *Z_PermanentDeleteBotArgs, returns *Z_PermanentDeleteBotReturns) error { + if hook, ok := s.impl.(interface { + PermanentDeleteBot(botUserId string) *model.AppError + }); ok { + returns.A = hook.PermanentDeleteBot(args.A) + } else { + return encodableError(fmt.Errorf("API PermanentDeleteBot called but not implemented.")) + } + return nil +} diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index e4f9d36186..90adddf35b 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -87,6 +87,31 @@ func (_m *API) CopyFileInfos(userId string, fileIds []string) ([]string, *model. return r0, r1 } +// CreateBot provides a mock function with given fields: bot +func (_m *API) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) { + ret := _m.Called(bot) + + var r0 *model.Bot + if rf, ok := ret.Get(0).(func(*model.Bot) *model.Bot); ok { + r0 = rf(bot) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Bot) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(*model.Bot) *model.AppError); ok { + r1 = rf(bot) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // CreateChannel provides a mock function with given fields: channel func (_m *API) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { ret := _m.Called(channel) @@ -365,6 +390,56 @@ func (_m *API) EnablePlugin(id string) *model.AppError { return r0 } +// GetBot provides a mock function with given fields: botUserId, includeDeleted +func (_m *API) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) { + ret := _m.Called(botUserId, includeDeleted) + + var r0 *model.Bot + if rf, ok := ret.Get(0).(func(string, bool) *model.Bot); ok { + r0 = rf(botUserId, includeDeleted) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Bot) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, bool) *model.AppError); ok { + r1 = rf(botUserId, includeDeleted) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetBots provides a mock function with given fields: options +func (_m *API) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) { + ret := _m.Called(options) + + var r0 []*model.Bot + if rf, ok := ret.Get(0).(func(*model.BotGetOptions) []*model.Bot); ok { + r0 = rf(options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Bot) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(*model.BotGetOptions) *model.AppError); ok { + r1 = rf(options) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetChannel provides a mock function with given fields: channelId func (_m *API) GetChannel(channelId string) (*model.Channel, *model.AppError) { ret := _m.Called(channelId) @@ -1884,6 +1959,47 @@ func (_m *API) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppE return r0 } +// PatchBot provides a mock function with given fields: botUserId, botPatch +func (_m *API) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) { + ret := _m.Called(botUserId, botPatch) + + var r0 *model.Bot + if rf, ok := ret.Get(0).(func(string, *model.BotPatch) *model.Bot); ok { + r0 = rf(botUserId, botPatch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Bot) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, *model.BotPatch) *model.AppError); ok { + r1 = rf(botUserId, botPatch) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// PermanentDeleteBot provides a mock function with given fields: botUserId +func (_m *API) PermanentDeleteBot(botUserId string) *model.AppError { + ret := _m.Called(botUserId) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(botUserId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // PublishWebSocketEvent provides a mock function with given fields: event, payload, broadcast func (_m *API) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) { _m.Called(event, payload, broadcast) @@ -2215,6 +2331,31 @@ func (_m *API) UnregisterCommand(teamId string, trigger string) error { return r0 } +// UpdateBotActive provides a mock function with given fields: botUserId, active +func (_m *API) UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) { + ret := _m.Called(botUserId, active) + + var r0 *model.Bot + if rf, ok := ret.Get(0).(func(string, bool) *model.Bot); ok { + r0 = rf(botUserId, active) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Bot) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, bool) *model.AppError); ok { + r1 = rf(botUserId, active) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // UpdateChannel provides a mock function with given fields: channel func (_m *API) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { ret := _m.Called(channel) diff --git a/store/layered_store.go b/store/layered_store.go index fc47aff26e..273358612d 100644 --- a/store/layered_store.go +++ b/store/layered_store.go @@ -87,6 +87,10 @@ func (s *LayeredStore) User() UserStore { return s.DatabaseLayer.User() } +func (s *LayeredStore) Bot() BotStore { + return s.DatabaseLayer.Bot() +} + func (s *LayeredStore) Audit() AuditStore { return s.DatabaseLayer.Audit() } diff --git a/store/sqlstore/bot_store.go b/store/sqlstore/bot_store.go new file mode 100644 index 0000000000..9557be6865 --- /dev/null +++ b/store/sqlstore/bot_store.go @@ -0,0 +1,253 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sqlstore + +import ( + "database/sql" + "net/http" + "strings" + + "github.com/mattermost/mattermost-server/einterfaces" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" +) + +// bot is a subset of the model.Bot type, omitting the model.User fields. +type bot struct { + UserId string `json:"user_id"` + Description string `json:"description"` + OwnerId string `json:"owner_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` +} + +func botFromModel(b *model.Bot) *bot { + return &bot{ + UserId: b.UserId, + Description: b.Description, + OwnerId: b.OwnerId, + CreateAt: b.CreateAt, + UpdateAt: b.UpdateAt, + DeleteAt: b.DeleteAt, + } +} + +// SqlBotStore is a store for managing bots in the database. +// Bots are otherwise normal users with extra metadata record in the Bots table. The primary key +// for a bot matches the primary key value for corresponding User record. +type SqlBotStore struct { + SqlStore + metrics einterfaces.MetricsInterface +} + +// NewSqlBotStore creates an instance of SqlBotStore, registering the table schema in question. +func NewSqlBotStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) store.BotStore { + us := &SqlBotStore{ + SqlStore: sqlStore, + metrics: metrics, + } + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(bot{}, "Bots").SetKeys(false, "UserId") + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("Description").SetMaxSize(1024) + table.ColMap("OwnerId").SetMaxSize(model.BOT_CREATOR_ID_MAX_RUNES) + } + + return us +} + +func (us SqlBotStore) CreateIndexesIfNotExists() { +} + +// traceBot is a helper function for adding to a bot trace when logging. +func traceBot(bot *model.Bot, extra map[string]interface{}) map[string]interface{} { + trace := make(map[string]interface{}) + for key, value := range bot.Trace() { + trace[key] = value + } + for key, value := range extra { + trace[key] = value + } + + return trace +} + +// Get fetches the given bot in the database. +func (us SqlBotStore) Get(botUserId string, includeDeleted bool) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var excludeDeletedSql = "AND b.DeleteAt = 0" + if includeDeleted { + excludeDeletedSql = "" + } + + var bot *model.Bot + if err := us.GetReplica().SelectOne(&bot, ` + SELECT + b.UserId, + u.Username, + u.FirstName AS DisplayName, + b.Description, + b.OwnerId, + b.CreateAt, + b.UpdateAt, + b.DeleteAt + FROM + Bots b + JOIN + Users u ON (u.Id = b.UserId) + WHERE + b.UserId = :user_id + `+excludeDeletedSql+` + `, map[string]interface{}{ + "user_id": botUserId, + }); err == sql.ErrNoRows { + result.Err = model.MakeBotNotFoundError(botUserId) + } else if err != nil { + result.Err = model.NewAppError("SqlBotStore.Get", "store.sql_bot.get.app_error", map[string]interface{}{"user_id": botUserId}, err.Error(), http.StatusInternalServerError) + } else { + result.Data = bot + } + }) +} + +// GetAll fetches from all bots in the database. +func (us SqlBotStore) GetAll(options *model.BotGetOptions) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + params := map[string]interface{}{ + "offset": options.Page * options.PerPage, + "limit": options.PerPage, + } + + var conditions []string + var conditionsSql string + var additionalJoin string + + if !options.IncludeDeleted { + conditions = append(conditions, "b.DeleteAt = 0") + } + if options.OwnerId != "" { + conditions = append(conditions, "b.OwnerId = :creator_id") + params["creator_id"] = options.OwnerId + } + if options.OnlyOrphaned { + additionalJoin = "JOIN Users o ON (o.Id = b.OwnerId)" + conditions = append(conditions, "o.DeleteAt != 0") + } + + if len(conditions) > 0 { + conditionsSql = "WHERE " + strings.Join(conditions, " AND ") + } + + sql := ` + SELECT + b.UserId, + u.Username, + u.FirstName AS DisplayName, + b.Description, + b.OwnerId, + b.CreateAt, + b.UpdateAt, + b.DeleteAt + FROM + Bots b + JOIN + Users u ON (u.Id = b.UserId) + ` + additionalJoin + ` + ` + conditionsSql + ` + ORDER BY + b.CreateAt ASC, + u.Username ASC + LIMIT + :limit + OFFSET + :offset + ` + + var data []*model.Bot + if _, err := us.GetReplica().Select(&data, sql, params); err != nil { + result.Err = model.NewAppError("SqlBotStore.GetAll", "store.sql_bot.get_all.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + result.Data = data + }) +} + +// Save persists a new bot to the database. +// It assumes the corresponding user was saved via the user store. +func (us SqlBotStore) Save(bot *model.Bot) store.StoreChannel { + bot = bot.Clone() + + return store.Do(func(result *store.StoreResult) { + bot.PreSave() + if result.Err = bot.IsValid(); result.Err != nil { + return + } + + if err := us.GetMaster().Insert(botFromModel(bot)); err != nil { + result.Err = model.NewAppError("SqlBotStore.Save", "store.sql_bot.save.app_error", bot.Trace(), err.Error(), http.StatusInternalServerError) + return + } + + result.Data = bot + }) +} + +// Update persists an updated bot to the database. +// It assumes the corresponding user was updated via the user store. +func (us SqlBotStore) Update(bot *model.Bot) store.StoreChannel { + bot = bot.Clone() + + return store.Do(func(result *store.StoreResult) { + bot.PreUpdate() + if result.Err = bot.IsValid(); result.Err != nil { + return + } + + oldBotResult := <-us.Get(bot.UserId, true) + if oldBotResult.Err != nil { + result.Err = oldBotResult.Err + return + } + oldBot := oldBotResult.Data.(*model.Bot) + + oldBot.Description = bot.Description + oldBot.OwnerId = bot.OwnerId + oldBot.UpdateAt = bot.UpdateAt + oldBot.DeleteAt = bot.DeleteAt + bot = oldBot + + if count, err := us.GetMaster().Update(botFromModel(bot)); err != nil { + result.Err = model.NewAppError("SqlBotStore.Update", "store.sql_bot.update.updating.app_error", bot.Trace(), err.Error(), http.StatusInternalServerError) + } else if count != 1 { + result.Err = model.NewAppError("SqlBotStore.Update", "store.sql_bot.update.app_error", traceBot(bot, map[string]interface{}{"count": count}), "", http.StatusInternalServerError) + } + + result.Data = bot + }) +} + +// PermanentDelete removes the bot from the database altogether. +// If the corresponding user is to be deleted, it must be done via the user store. +func (us SqlBotStore) PermanentDelete(botUserId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + userResult := <-us.User().PermanentDelete(botUserId) + if userResult.Err != nil { + result.Err = userResult.Err + return + } + + if _, err := us.GetMaster().Exec(` + DELETE FROM + Bots + WHERE + UserId = :user_id + `, map[string]interface{}{ + "user_id": botUserId, + }); err != nil { + result.Err = model.NewAppError("SqlBotStore.Update", "store.sql_bot.delete.app_error", map[string]interface{}{"user_id": botUserId}, err.Error(), http.StatusBadRequest) + } + }) +} diff --git a/store/sqlstore/bot_store_test.go b/store/sqlstore/bot_store_test.go new file mode 100644 index 0000000000..ab0010b43a --- /dev/null +++ b/store/sqlstore/bot_store_test.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sqlstore + +import ( + "testing" + + "github.com/mattermost/mattermost-server/store/storetest" +) + +func TestBotStore(t *testing.T) { + StoreTest(t, storetest.TestBotStore) +} diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 945a8fb7e6..7c03e75f43 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -73,6 +73,7 @@ type SqlStore interface { Channel() store.ChannelStore Post() store.PostStore User() store.UserStore + Bot() store.BotStore Audit() store.AuditStore ClusterDiscovery() store.ClusterDiscoveryStore Compliance() store.ComplianceStore diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index 2ed51c7139..4a35274460 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -71,6 +71,7 @@ type SqlSupplierOldStores struct { channel store.ChannelStore post store.PostStore user store.UserStore + bot store.BotStore audit store.AuditStore cluster store.ClusterDiscoveryStore compliance store.ComplianceStore @@ -126,6 +127,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.oldStores.channel = NewSqlChannelStore(supplier, metrics) supplier.oldStores.post = NewSqlPostStore(supplier, metrics) supplier.oldStores.user = NewSqlUserStore(supplier, metrics) + supplier.oldStores.bot = NewSqlBotStore(supplier, metrics) supplier.oldStores.audit = NewSqlAuditStore(supplier) supplier.oldStores.cluster = NewSqlClusterDiscoveryStore(supplier) supplier.oldStores.compliance = NewSqlComplianceStore(supplier) @@ -167,6 +169,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter supplier.oldStores.channel.(*SqlChannelStore).CreateIndexesIfNotExists() supplier.oldStores.post.(*SqlPostStore).CreateIndexesIfNotExists() supplier.oldStores.user.(*SqlUserStore).CreateIndexesIfNotExists() + supplier.oldStores.bot.(*SqlBotStore).CreateIndexesIfNotExists() supplier.oldStores.audit.(*SqlAuditStore).CreateIndexesIfNotExists() supplier.oldStores.compliance.(*SqlComplianceStore).CreateIndexesIfNotExists() supplier.oldStores.session.(*SqlSessionStore).CreateIndexesIfNotExists() @@ -936,6 +939,10 @@ func (ss *SqlSupplier) User() store.UserStore { return ss.oldStores.user } +func (ss *SqlSupplier) Bot() store.BotStore { + return ss.oldStores.bot +} + func (ss *SqlSupplier) Session() store.SessionStore { return ss.oldStores.session } diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 2356d498db..a965842c46 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -4,12 +4,15 @@ package sqlstore import ( + "database/sql" "encoding/json" "fmt" "os" "strings" "time" + "github.com/pkg/errors" + "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/services/timezones" @@ -55,10 +58,11 @@ const ( ) const ( - EXIT_VERSION_SAVE_MISSING = 1001 - EXIT_TOO_OLD = 1002 - EXIT_VERSION_SAVE = 1003 - EXIT_THEME_MIGRATION = 1004 + EXIT_VERSION_SAVE_MISSING = 1001 + EXIT_TOO_OLD = 1002 + EXIT_VERSION_SAVE = 1003 + EXIT_THEME_MIGRATION = 1004 + EXIT_ROLE_MIGRATION_FAILED = 1005 ) func UpgradeDatabase(sqlStore SqlStore) { @@ -554,6 +558,33 @@ func UpgradeDatabaseToVersion57(sqlStore SqlStore) { } } +func getRole(sqlStore SqlStore, name string) (*model.Role, error) { + var dbRole Role + + if err := sqlStore.GetReplica().SelectOne(&dbRole, "SELECT * from Roles WHERE Name = :Name", map[string]interface{}{"Name": name}); err != nil { + if err == sql.ErrNoRows { + return nil, errors.Wrapf(err, "failed to find role %s", name) + } else { + return nil, errors.Wrapf(err, "failed to query role %s", name) + } + } + + return dbRole.ToModel(), nil +} + +func saveRole(sqlStore SqlStore, role *model.Role) error { + dbRole := NewRoleFromModel(role) + + dbRole.UpdateAt = model.GetMillis() + if rowsChanged, err := sqlStore.GetMaster().Update(dbRole); err != nil { + return errors.Wrap(err, "failed to update role") + } else if rowsChanged != 1 { + return errors.New("found no role to update") + } + + return nil +} + func UpgradeDatabaseToVersion58(sqlStore SqlStore) { if shouldPerformUpgrade(sqlStore, VERSION_5_7_0, VERSION_5_8_0) { // idx_channels_txt was removed in `UpgradeDatabaseToVersion50`, but merged as part of @@ -583,6 +614,26 @@ func UpgradeDatabaseToVersion59(sqlStore SqlStore) { func UpgradeDatabaseToVersion510(sqlStore SqlStore) { // if shouldPerformUpgrade(sqlStore, VERSION_5_9_0, VERSION_5_10_0) { + // Grant new bot permissions to the system admin. Ideally we'd use the RoleStore directly, + // but it uses the new supplier model, which isn't initialized in the UpgradeDatabase code + // path. Also, the role won't exist for new servers, so don't fail on fetch, and don't + // bother inserting since it will be created with the new permissions anyway. + if role, err := getRole(sqlStore, model.SYSTEM_ADMIN_ROLE_ID); err != nil { + mlog.Warn("Failed to find role " + model.SYSTEM_ADMIN_ROLE_ID + " for upgrade: " + err.Error()) + } else { + role.Permissions = append(role.Permissions, model.PERMISSION_CREATE_BOT.Id) + role.Permissions = append(role.Permissions, model.PERMISSION_READ_BOTS.Id) + role.Permissions = append(role.Permissions, model.PERMISSION_READ_OTHERS_BOTS.Id) + role.Permissions = append(role.Permissions, model.PERMISSION_MANAGE_BOTS.Id) + role.Permissions = append(role.Permissions, model.PERMISSION_MANAGE_OTHERS_BOTS.Id) + + if err := saveRole(sqlStore, role); err != nil { + mlog.Critical(err.Error()) + time.Sleep(time.Second) + os.Exit(EXIT_ROLE_MIGRATION_FAILED) + } + } + // saveSchemaVersion(sqlStore, VERSION_5_10_0) // } } diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index f50a13beea..0e1a99e4ed 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -68,8 +68,9 @@ func NewSqlUserStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) st } us.usersQuery = sq. - Select("u.*"). - From("Users u") + Select("u.*", "b.UserId IS NOT NULL AS IsBot"). + From("Users u"). + LeftJoin("Bots b ON ( b.UserId = u.Id )") if us.DriverName() == model.DATABASE_DRIVER_POSTGRES { us.usersQuery = us.usersQuery.PlaceholderFormat(sq.Dollar) @@ -1036,16 +1037,6 @@ func (us SqlUserStore) VerifyEmail(userId, email string) store.StoreChannel { }) } -func (us SqlUserStore) GetTotalUsersCount() store.StoreChannel { - return store.Do(func(result *store.StoreResult) { - if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users"); err != nil { - result.Err = model.NewAppError("SqlUserStore.GetTotalUsersCount", "store.sql_user.get_total_users_count.app_error", nil, err.Error(), http.StatusInternalServerError) - } else { - result.Data = count - } - }) -} - func (us SqlUserStore) PermanentDelete(userId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { if _, err := us.GetMaster().Exec("DELETE FROM Users WHERE Id = :UserId", map[string]interface{}{"UserId": userId}); err != nil { @@ -1054,20 +1045,45 @@ func (us SqlUserStore) PermanentDelete(userId string) store.StoreChannel { }) } -func (us SqlUserStore) AnalyticsUniqueUserCount(teamId string) store.StoreChannel { +func (us SqlUserStore) Count(options model.UserCountOptions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - query := "" - if len(teamId) > 0 { - query = "SELECT COUNT(DISTINCT Users.Email) From Users, TeamMembers WHERE TeamMembers.TeamId = :TeamId AND Users.Id = TeamMembers.UserId AND TeamMembers.DeleteAt = 0 AND Users.DeleteAt = 0" - } else { - query = "SELECT COUNT(DISTINCT Email) FROM Users WHERE DeleteAt = 0" + query := sq.Select("COUNT(Users.Id)").From("Users") + + if !options.IncludeDeleted { + query = query.Where("Users.DeleteAt = 0") } - v, err := us.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}) - if err != nil { - result.Err = model.NewAppError("SqlUserStore.AnalyticsUniqueUserCount", "store.sql_user.analytics_unique_user_count.app_error", nil, err.Error(), http.StatusInternalServerError) + if options.IncludeBotAccounts { + if options.ExcludeRegularUsers { + query = query.Join("Bots ON Users.Id = Bots.UserId") + } } else { - result.Data = v + query = query.LeftJoin("Bots ON Users.Id = Bots.UserId").Where("Bots.UserId IS NULL") + if options.ExcludeRegularUsers { + // Currenty this doesn't make sense because it will always return 0 + result.Err = model.NewAppError("SqlUserStore.Count", "UserCountOptions don't make sense", nil, "", http.StatusInternalServerError) + return + } + } + + if options.TeamId != "" { + query = query.LeftJoin("TeamMembers ON Users.Id = TeamMembers.UserId").Where("TeamMembers.TeamId = ? AND TeamMembers.DeleteAt = 0", options.TeamId) + } + + if us.DriverName() == model.DATABASE_DRIVER_POSTGRES { + query = query.PlaceholderFormat(sq.Dollar) + } + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlUserStore.Get", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + if count, err := us.GetReplica().SelectInt(queryString, args...); err != nil { + result.Err = model.NewAppError("SqlUserStore.Count", "store.sql_user.get_total_users_count.app_error", nil, err.Error(), http.StatusInternalServerError) + } else { + result.Data = count } }) } diff --git a/store/store.go b/store/store.go index 7db4149169..306a370066 100644 --- a/store/store.go +++ b/store/store.go @@ -43,6 +43,7 @@ type Store interface { Channel() ChannelStore Post() PostStore User() UserStore + Bot() BotStore Audit() AuditStore ClusterDiscovery() ClusterDiscoveryStore Compliance() ComplianceStore @@ -264,10 +265,8 @@ type UserStore interface { GetEtagForAllProfiles() StoreChannel GetEtagForProfiles(teamId string) StoreChannel UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel - GetTotalUsersCount() StoreChannel GetSystemAdminProfiles() StoreChannel PermanentDelete(userId string) StoreChannel - AnalyticsUniqueUserCount(teamId string) StoreChannel AnalyticsActiveCount(time int64) StoreChannel GetUnreadCount(userId string) StoreChannel GetUnreadCountForChannel(userId string, channelId string) StoreChannel @@ -286,6 +285,15 @@ type UserStore interface { ClearAllCustomRoleAssignments() StoreChannel InferSystemInstallDate() StoreChannel GetAllAfter(limit int, afterId string) StoreChannel + Count(options model.UserCountOptions) StoreChannel +} + +type BotStore interface { + Get(userId string, includeDeleted bool) StoreChannel + GetAll(options *model.BotGetOptions) StoreChannel + Save(bot *model.Bot) StoreChannel + Update(bot *model.Bot) StoreChannel + PermanentDelete(userId string) StoreChannel } type SessionStore interface { diff --git a/store/storetest/bot_store.go b/store/storetest/bot_store.go new file mode 100644 index 0000000000..debb8b881e --- /dev/null +++ b/store/storetest/bot_store.go @@ -0,0 +1,441 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package storetest + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" +) + +func makeBotWithUser(ss store.Store, bot *model.Bot) (*model.Bot, *model.User) { + user := store.Must(ss.User().Save(model.UserFromBot(bot))).(*model.User) + + bot.UserId = user.Id + bot = store.Must(ss.Bot().Save(bot)).(*model.Bot) + + return bot, user +} + +func TestBotStore(t *testing.T, ss store.Store) { + t.Run("Get", func(t *testing.T) { testBotStoreGet(t, ss) }) + t.Run("GetAll", func(t *testing.T) { testBotStoreGetAll(t, ss) }) + t.Run("Save", func(t *testing.T) { testBotStoreSave(t, ss) }) + t.Run("Update", func(t *testing.T) { testBotStoreUpdate(t, ss) }) + t.Run("PermanentDelete", func(t *testing.T) { testBotStorePermanentDelete(t, ss) }) +} + +func testBotStoreGet(t *testing.T, ss store.Store) { + deletedBot, _ := makeBotWithUser(ss, &model.Bot{ + Username: "deleted_bot", + Description: "A deleted bot", + OwnerId: model.NewId(), + }) + deletedBot.DeleteAt = 1 + deletedBot = store.Must(ss.Bot().Update(deletedBot)).(*model.Bot) + defer func() { store.Must(ss.Bot().PermanentDelete(deletedBot.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(deletedBot.UserId)) }() + + permanentlyDeletedBot, _ := makeBotWithUser(ss, &model.Bot{ + Username: "permanently_deleted_bot", + Description: "A permanently deleted bot", + OwnerId: model.NewId(), + DeleteAt: 0, + }) + store.Must(ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId)) + + b1, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b1", + Description: "The first bot", + OwnerId: model.NewId(), + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b1.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b1.UserId)) }() + + b2, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b2", + Description: "The second bot", + OwnerId: model.NewId(), + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b2.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b2.UserId)) }() + + t.Run("get non-existent bot", func(t *testing.T) { + result := <-ss.Bot().Get("unknown", false) + require.NotNil(t, result.Err) + require.Equal(t, http.StatusNotFound, result.Err.StatusCode) + }) + + t.Run("get deleted bot", func(t *testing.T) { + result := <-ss.Bot().Get(deletedBot.UserId, false) + require.NotNil(t, result.Err) + require.Equal(t, http.StatusNotFound, result.Err.StatusCode) + }) + + t.Run("get deleted bot, include deleted", func(t *testing.T) { + result := <-ss.Bot().Get(deletedBot.UserId, true) + require.Nil(t, result.Err) + require.Equal(t, deletedBot, result.Data.(*model.Bot)) + }) + + t.Run("get permanently deleted bot", func(t *testing.T) { + result := <-ss.Bot().Get(permanentlyDeletedBot.UserId, false) + require.NotNil(t, result.Err) + require.Equal(t, http.StatusNotFound, result.Err.StatusCode) + }) + + t.Run("get bot 1", func(t *testing.T) { + result := <-ss.Bot().Get(b1.UserId, false) + require.Nil(t, result.Err) + require.Equal(t, b1, result.Data.(*model.Bot)) + }) + + t.Run("get bot 2", func(t *testing.T) { + result := <-ss.Bot().Get(b2.UserId, false) + require.Nil(t, result.Err) + require.Equal(t, b2, result.Data.(*model.Bot)) + }) +} + +func testBotStoreGetAll(t *testing.T, ss store.Store) { + OwnerId1 := model.NewId() + OwnerId2 := model.NewId() + + deletedBot, _ := makeBotWithUser(ss, &model.Bot{ + Username: "deleted_bot", + Description: "A deleted bot", + OwnerId: OwnerId1, + }) + deletedBot.DeleteAt = 1 + deletedBot = store.Must(ss.Bot().Update(deletedBot)).(*model.Bot) + defer func() { store.Must(ss.Bot().PermanentDelete(deletedBot.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(deletedBot.UserId)) }() + + permanentlyDeletedBot, _ := makeBotWithUser(ss, &model.Bot{ + Username: "permanently_deleted_bot", + Description: "A permanently deleted bot", + OwnerId: OwnerId1, + DeleteAt: 0, + }) + store.Must(ss.Bot().PermanentDelete(permanentlyDeletedBot.UserId)) + + b1, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b1", + Description: "The first bot", + OwnerId: OwnerId1, + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b1.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b1.UserId)) }() + + b2, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b2", + Description: "The second bot", + OwnerId: OwnerId1, + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b2.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b2.UserId)) }() + + t.Run("get original bots", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b1, + b2, + }, result.Data.([]*model.Bot)) + }) + + b3, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b3", + Description: "The third bot", + OwnerId: OwnerId1, + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b3.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b3.UserId)) }() + + b4, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b4", + Description: "The fourth bot", + OwnerId: OwnerId2, + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b4.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b4.UserId)) }() + + deletedUser := model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + if err := (<-ss.User().Save(&deletedUser)).Err; err != nil { + t.Fatal("couldn't save user", err) + } + deletedUser.DeleteAt = model.GetMillis() + if err := (<-ss.User().Update(&deletedUser, true)).Err; err != nil { + t.Fatal("couldn't delete user", err) + } + defer func() { store.Must(ss.User().PermanentDelete(deletedUser.Id)) }() + ob5, _ := makeBotWithUser(ss, &model.Bot{ + Username: "ob5", + Description: "Orphaned bot 5", + OwnerId: deletedUser.Id, + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b4.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b4.UserId)) }() + + t.Run("get newly created bot stoo", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b1, + b2, + b3, + b4, + ob5, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get orphaned", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OnlyOrphaned: true}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + ob5, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get page=0, per_page=2", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 2}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b1, + b2, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get page=1, limit=2", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 1, PerPage: 2}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b3, + b4, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get page=5, perpage=1000", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 5, PerPage: 1000}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{}, result.Data.([]*model.Bot)) + }) + + t.Run("get offset=0, limit=2, include deleted", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 2, IncludeDeleted: true}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + deletedBot, + b1, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get offset=2, limit=2, include deleted", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 1, PerPage: 2, IncludeDeleted: true}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b2, + b3, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get offset=0, limit=10, creator id 1", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OwnerId: OwnerId1}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b1, + b2, + b3, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get offset=0, limit=10, creator id 2", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, OwnerId: OwnerId2}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b4, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get offset=0, limit=10, include deleted, creator id 1", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, IncludeDeleted: true, OwnerId: OwnerId1}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + deletedBot, + b1, + b2, + b3, + }, result.Data.([]*model.Bot)) + }) + + t.Run("get offset=0, limit=10, include deleted, creator id 2", func(t *testing.T) { + result := <-ss.Bot().GetAll(&model.BotGetOptions{Page: 0, PerPage: 10, IncludeDeleted: true, OwnerId: OwnerId2}) + require.Nil(t, result.Err) + require.Equal(t, []*model.Bot{ + b4, + }, result.Data.([]*model.Bot)) + }) +} + +func testBotStoreSave(t *testing.T, ss store.Store) { + t.Run("invalid bot", func(t *testing.T) { + bot := &model.Bot{ + UserId: model.NewId(), + Username: "invalid bot", + Description: "description", + } + + result := <-ss.Bot().Save(bot) + require.NotNil(t, result.Err) + require.Equal(t, "model.bot.is_valid.username.app_error", result.Err.Id) + }) + + t.Run("normal bot", func(t *testing.T) { + bot := &model.Bot{ + Username: "normal_bot", + Description: "description", + OwnerId: model.NewId(), + } + + user := store.Must(ss.User().Save(model.UserFromBot(bot))).(*model.User) + defer func() { store.Must(ss.User().PermanentDelete(user.Id)) }() + bot.UserId = user.Id + + result := <-ss.Bot().Save(bot) + require.Nil(t, result.Err) + defer func() { store.Must(ss.Bot().PermanentDelete(bot.UserId)) }() + + // Verify the returned bot matches the saved bot, modulo expected changes + returnedNewBot := result.Data.(*model.Bot) + require.NotEqual(t, 0, returnedNewBot.CreateAt) + require.NotEqual(t, 0, returnedNewBot.UpdateAt) + require.Equal(t, returnedNewBot.CreateAt, returnedNewBot.UpdateAt) + bot.UserId = returnedNewBot.UserId + bot.CreateAt = returnedNewBot.CreateAt + bot.UpdateAt = returnedNewBot.UpdateAt + bot.DeleteAt = 0 + require.Equal(t, bot, returnedNewBot) + + // Verify the actual bot in the database matches the saved bot. + result = <-ss.Bot().Get(bot.UserId, false) + require.Nil(t, result.Err) + actualNewBot := result.Data.(*model.Bot) + require.Equal(t, bot, actualNewBot) + }) +} + +func testBotStoreUpdate(t *testing.T, ss store.Store) { + t.Run("invalid bot should fail to update", func(t *testing.T) { + existingBot, _ := makeBotWithUser(ss, &model.Bot{ + Username: "existing_bot", + OwnerId: model.NewId(), + }) + defer func() { store.Must(ss.Bot().PermanentDelete(existingBot.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(existingBot.UserId)) }() + + bot := existingBot.Clone() + bot.Username = "invalid username" + result := <-ss.Bot().Update(bot) + require.NotNil(t, result.Err) + require.Equal(t, "model.bot.is_valid.username.app_error", result.Err.Id) + }) + + t.Run("existing bot should update", func(t *testing.T) { + existingBot, _ := makeBotWithUser(ss, &model.Bot{ + Username: "existing_bot", + OwnerId: model.NewId(), + }) + defer func() { store.Must(ss.Bot().PermanentDelete(existingBot.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(existingBot.UserId)) }() + + bot := existingBot.Clone() + bot.OwnerId = model.NewId() + bot.Description = "updated description" + bot.CreateAt = 999999 // Ignored + bot.UpdateAt = 999999 // Ignored + bot.DeleteAt = 100000 // Allowed + + result := <-ss.Bot().Update(bot) + require.Nil(t, result.Err) + + // Verify the returned bot matches the updated bot, modulo expected timestamp changes + returnedBot := result.Data.(*model.Bot) + require.Equal(t, existingBot.CreateAt, returnedBot.CreateAt) + require.NotEqual(t, bot.UpdateAt, returnedBot.UpdateAt, "update should have advanced UpdateAt") + require.True(t, returnedBot.UpdateAt > bot.UpdateAt, "update should have advanced UpdateAt") + require.NotEqual(t, 99999, returnedBot.UpdateAt, "should have ignored user-provided UpdateAt") + bot.CreateAt = returnedBot.CreateAt + bot.UpdateAt = returnedBot.UpdateAt + + // Verify the actual (now deleted) bot in the database + result = <-ss.Bot().Get(bot.UserId, true) + require.Nil(t, result.Err) + require.Equal(t, bot, result.Data.(*model.Bot)) + }) + + t.Run("deleted bot should update, restoring", func(t *testing.T) { + existingBot, _ := makeBotWithUser(ss, &model.Bot{ + Username: "existing_bot", + OwnerId: model.NewId(), + }) + defer func() { store.Must(ss.Bot().PermanentDelete(existingBot.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(existingBot.UserId)) }() + + existingBot.DeleteAt = 100000 + existingBot = store.Must(ss.Bot().Update(existingBot)).(*model.Bot) + + bot := existingBot.Clone() + bot.DeleteAt = 0 + + result := <-ss.Bot().Update(bot) + require.Nil(t, result.Err) + + // Verify the returned bot matches the updated bot, modulo expected timestamp changes + returnedBot := result.Data.(*model.Bot) + require.EqualValues(t, 0, returnedBot.DeleteAt) + bot.UpdateAt = returnedBot.UpdateAt + + // Verify the actual bot in the database + result = <-ss.Bot().Get(bot.UserId, false) + require.Nil(t, result.Err) + require.Equal(t, bot, result.Data.(*model.Bot)) + }) +} + +func testBotStorePermanentDelete(t *testing.T, ss store.Store) { + b1, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b1", + OwnerId: model.NewId(), + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b1.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b1.UserId)) }() + + b2, _ := makeBotWithUser(ss, &model.Bot{ + Username: "b2", + OwnerId: model.NewId(), + }) + defer func() { store.Must(ss.Bot().PermanentDelete(b2.UserId)) }() + defer func() { store.Must(ss.User().PermanentDelete(b2.UserId)) }() + + t.Run("permanently delete a non-existent bot", func(t *testing.T) { + result := <-ss.Bot().PermanentDelete("unknown") + require.Nil(t, result.Err) + }) + + t.Run("permanently delete bot", func(t *testing.T) { + result := <-ss.Bot().PermanentDelete(b1.UserId) + require.Nil(t, result.Err) + + result = <-ss.Bot().Get(b1.UserId, false) + require.NotNil(t, result.Err) + require.Equal(t, http.StatusNotFound, result.Err.StatusCode) + }) +} diff --git a/store/storetest/mocks/BotStore.go b/store/storetest/mocks/BotStore.go new file mode 100644 index 0000000000..65c2b92497 --- /dev/null +++ b/store/storetest/mocks/BotStore.go @@ -0,0 +1,94 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +// Regenerate this file using `make store-mocks`. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import model "github.com/mattermost/mattermost-server/model" +import store "github.com/mattermost/mattermost-server/store" + +// BotStore is an autogenerated mock type for the BotStore type +type BotStore struct { + mock.Mock +} + +// Get provides a mock function with given fields: userId, includeDeleted +func (_m *BotStore) Get(userId string, includeDeleted bool) store.StoreChannel { + ret := _m.Called(userId, includeDeleted) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string, bool) store.StoreChannel); ok { + r0 = rf(userId, includeDeleted) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// GetAll provides a mock function with given fields: options +func (_m *BotStore) GetAll(options *model.BotGetOptions) store.StoreChannel { + ret := _m.Called(options) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(*model.BotGetOptions) store.StoreChannel); ok { + r0 = rf(options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// PermanentDelete provides a mock function with given fields: userId +func (_m *BotStore) PermanentDelete(userId string) store.StoreChannel { + ret := _m.Called(userId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// Save provides a mock function with given fields: bot +func (_m *BotStore) Save(bot *model.Bot) store.StoreChannel { + ret := _m.Called(bot) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(*model.Bot) store.StoreChannel); ok { + r0 = rf(bot) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + +// Update provides a mock function with given fields: bot +func (_m *BotStore) Update(bot *model.Bot) store.StoreChannel { + ret := _m.Called(bot) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(*model.Bot) store.StoreChannel); ok { + r0 = rf(bot) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go index 99c846bd15..e1fbea7b4b 100644 --- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go +++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go @@ -30,6 +30,22 @@ func (_m *LayeredStoreDatabaseLayer) Audit() store.AuditStore { return r0 } +// Bot provides a mock function with given fields: +func (_m *LayeredStoreDatabaseLayer) Bot() store.BotStore { + ret := _m.Called() + + var r0 store.BotStore + if rf, ok := ret.Get(0).(func() store.BotStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.BotStore) + } + } + + return r0 +} + // Channel provides a mock function with given fields: func (_m *LayeredStoreDatabaseLayer) Channel() store.ChannelStore { ret := _m.Called() diff --git a/store/storetest/mocks/SqlStore.go b/store/storetest/mocks/SqlStore.go index e2c15a5fbd..f57138c015 100644 --- a/store/storetest/mocks/SqlStore.go +++ b/store/storetest/mocks/SqlStore.go @@ -58,6 +58,22 @@ func (_m *SqlStore) Audit() store.AuditStore { return r0 } +// Bot provides a mock function with given fields: +func (_m *SqlStore) Bot() store.BotStore { + ret := _m.Called() + + var r0 store.BotStore + if rf, ok := ret.Get(0).(func() store.BotStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.BotStore) + } + } + + return r0 +} + // Channel provides a mock function with given fields: func (_m *SqlStore) Channel() store.ChannelStore { ret := _m.Called() diff --git a/store/storetest/mocks/Store.go b/store/storetest/mocks/Store.go index 600c9e3f22..21746dd211 100644 --- a/store/storetest/mocks/Store.go +++ b/store/storetest/mocks/Store.go @@ -28,6 +28,22 @@ func (_m *Store) Audit() store.AuditStore { return r0 } +// Bot provides a mock function with given fields: +func (_m *Store) Bot() store.BotStore { + ret := _m.Called() + + var r0 store.BotStore + if rf, ok := ret.Get(0).(func() store.BotStore); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.BotStore) + } + } + + return r0 +} + // Channel provides a mock function with given fields: func (_m *Store) Channel() store.ChannelStore { ret := _m.Called() diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index e3c40f72eb..6a6be180b1 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -61,22 +61,6 @@ func (_m *UserStore) AnalyticsGetSystemAdminCount() store.StoreChannel { return r0 } -// AnalyticsUniqueUserCount provides a mock function with given fields: teamId -func (_m *UserStore) AnalyticsUniqueUserCount(teamId string) store.StoreChannel { - ret := _m.Called(teamId) - - var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { - r0 = rf(teamId) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(store.StoreChannel) - } - } - - return r0 -} - // ClearAllCustomRoleAssignments provides a mock function with given fields: func (_m *UserStore) ClearAllCustomRoleAssignments() store.StoreChannel { ret := _m.Called() @@ -98,6 +82,22 @@ func (_m *UserStore) ClearCaches() { _m.Called() } +// Count provides a mock function with given fields: options +func (_m *UserStore) Count(options model.UserCountOptions) store.StoreChannel { + ret := _m.Called(options) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(model.UserCountOptions) store.StoreChannel); ok { + r0 = rf(options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // Get provides a mock function with given fields: id func (_m *UserStore) Get(id string) store.StoreChannel { ret := _m.Called(id) @@ -498,22 +498,6 @@ func (_m *UserStore) GetSystemAdminProfiles() store.StoreChannel { return r0 } -// GetTotalUsersCount provides a mock function with given fields: -func (_m *UserStore) GetTotalUsersCount() store.StoreChannel { - ret := _m.Called() - - var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func() store.StoreChannel); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(store.StoreChannel) - } - } - - return r0 -} - // GetUnreadCount provides a mock function with given fields: userId func (_m *UserStore) GetUnreadCount(userId string) store.StoreChannel { ret := _m.Called(userId) diff --git a/store/storetest/store.go b/store/storetest/store.go index 76ec1c474d..6c29491219 100644 --- a/store/storetest/store.go +++ b/store/storetest/store.go @@ -23,6 +23,7 @@ type Store struct { ChannelStore mocks.ChannelStore PostStore mocks.PostStore UserStore mocks.UserStore + BotStore mocks.BotStore AuditStore mocks.AuditStore ClusterDiscoveryStore mocks.ClusterDiscoveryStore ComplianceStore mocks.ComplianceStore @@ -55,6 +56,7 @@ func (s *Store) Team() store.TeamStore { return &s.T func (s *Store) Channel() store.ChannelStore { return &s.ChannelStore } func (s *Store) Post() store.PostStore { return &s.PostStore } func (s *Store) User() store.UserStore { return &s.UserStore } +func (s *Store) Bot() store.BotStore { return &s.BotStore } func (s *Store) Audit() store.AuditStore { return &s.AuditStore } func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore } func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore } @@ -98,6 +100,7 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool { &s.ChannelStore, &s.PostStore, &s.UserStore, + &s.BotStore, &s.AuditStore, &s.ClusterDiscoveryStore, &s.ComplianceStore, diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 3138ceffb5..93f354457b 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -25,12 +25,14 @@ func TestUserStore(t *testing.T, ss store.Store) { require.Nil(t, result.Err, "failed cleaning up test user %s", u.Username) } + t.Run("Count", func(t *testing.T) { testCount(t, ss) }) + t.Run("AnalyticsGetInactiveUsersCount", func(t *testing.T) { testUserStoreAnalyticsGetInactiveUsersCount(t, ss) }) + t.Run("AnalyticsGetSystemAdminCount", func(t *testing.T) { testUserStoreAnalyticsGetSystemAdminCount(t, ss) }) t.Run("Save", func(t *testing.T) { testUserStoreSave(t, ss) }) t.Run("Update", func(t *testing.T) { testUserStoreUpdate(t, ss) }) t.Run("UpdateUpdateAt", func(t *testing.T) { testUserStoreUpdateUpdateAt(t, ss) }) t.Run("UpdateFailedPasswordAttempts", func(t *testing.T) { testUserStoreUpdateFailedPasswordAttempts(t, ss) }) t.Run("Get", func(t *testing.T) { testUserStoreGet(t, ss) }) - t.Run("UserCount", func(t *testing.T) { testUserCount(t, ss) }) t.Run("GetAllUsingAuthService", func(t *testing.T) { testGetAllUsingAuthService(t, ss) }) t.Run("GetAllProfiles", func(t *testing.T) { testUserStoreGetAllProfiles(t, ss) }) t.Run("GetProfiles", func(t *testing.T) { testUserStoreGetProfiles(t, ss) }) @@ -59,8 +61,6 @@ func TestUserStore(t *testing.T, ss store.Store) { t.Run("SearchInChannel", func(t *testing.T) { testUserStoreSearchInChannel(t, ss) }) t.Run("SearchNotInTeam", func(t *testing.T) { testUserStoreSearchNotInTeam(t, ss) }) t.Run("SearchWithoutTeam", func(t *testing.T) { testUserStoreSearchWithoutTeam(t, ss) }) - t.Run("AnalyticsGetInactiveUsersCount", func(t *testing.T) { testUserStoreAnalyticsGetInactiveUsersCount(t, ss) }) - t.Run("AnalyticsGetSystemAdminCount", func(t *testing.T) { testUserStoreAnalyticsGetSystemAdminCount(t, ss) }) t.Run("GetProfilesNotInTeam", func(t *testing.T) { testUserStoreGetProfilesNotInTeam(t, ss) }) t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testUserStoreClearAllCustomRoleAssignments(t, ss) }) t.Run("GetAllAfter", func(t *testing.T) { testUserStoreGetAllAfter(t, ss) }) @@ -259,6 +259,13 @@ func testUserStoreGet(t *testing.T, ss store.Store) { Email: MakeEmail(), Username: model.NewId(), })).(*model.User) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u2.Id, + Username: u2.Username, + OwnerId: u1.Id, + })) + u2.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u2.Id)) }() defer func() { store.Must(ss.User().PermanentDelete(u2.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) @@ -273,32 +280,19 @@ func testUserStoreGet(t *testing.T, ss store.Store) { actual := result.Data.(*model.User) require.Equal(t, u1, actual) + require.False(t, actual.IsBot) }) - t.Run("fetch user 2", func(t *testing.T) { + t.Run("fetch user 2, also a bot", func(t *testing.T) { result := <-ss.User().Get(u2.Id) require.Nil(t, result.Err) actual := result.Data.(*model.User) require.Equal(t, u2, actual) + require.True(t, actual.IsBot) }) } -func testUserCount(t *testing.T, ss store.Store) { - u1 := &model.User{} - u1.Email = MakeEmail() - store.Must(ss.User().Save(u1)) - defer func() { store.Must(ss.User().PermanentDelete(u1.Id)) }() - store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) - - if result := <-ss.User().GetTotalUsersCount(); result.Err != nil { - t.Fatal(result.Err) - } else { - count := result.Data.(int64) - require.False(t, count <= 0, "expected count > 0, got %d", count) - } -} - func testGetAllUsingAuthService(t *testing.T, ss store.Store) { teamId := model.NewId() @@ -325,6 +319,13 @@ func testGetAllUsingAuthService(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() t.Run("get by unknown auth service", func(t *testing.T) { @@ -372,6 +373,13 @@ func testUserStoreGetAllProfiles(t *testing.T, ss store.Store) { Email: MakeEmail(), Username: "u3" + model.NewId(), })).(*model.User) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() u4 := store.Must(ss.User().Save(&model.User{ @@ -529,6 +537,13 @@ func testUserStoreGetProfiles(t *testing.T, ss store.Store) { Email: MakeEmail(), Username: "u3" + model.NewId(), })).(*model.User) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) @@ -660,6 +675,13 @@ func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() c1 := store.Must(ss.Channel().Save(&model.Channel{ TeamId: teamId, @@ -741,6 +763,13 @@ func testUserStoreGetProfilesInChannelByStatus(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() c1 := store.Must(ss.Channel().Save(&model.Channel{ TeamId: teamId, @@ -827,6 +856,13 @@ func testUserStoreGetProfilesWithoutTeam(t *testing.T, ss store.Store) { Username: "u3" + model.NewId(), })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get, offset 0, limit 100", func(t *testing.T) { result := <-ss.User().GetProfilesWithoutTeam(0, 100) @@ -870,6 +906,13 @@ func testUserStoreGetAllProfilesInChannel(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() c1 := store.Must(ss.Channel().Save(&model.Channel{ TeamId: teamId, @@ -970,6 +1013,13 @@ func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() c1 := store.Must(ss.Channel().Save(&model.Channel{ TeamId: teamId, @@ -1068,6 +1118,13 @@ func testUserStoreGetProfilesByIds(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get u1 by id, no caching", func(t *testing.T) { result := <-ss.User().GetProfileByIds([]string{u1.Id}, false) @@ -1124,6 +1181,13 @@ func testUserStoreGetProfilesByUsernames(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: team2Id, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get by u1 and u2 usernames, team id 1", func(t *testing.T) { result := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, teamId) @@ -1181,6 +1245,13 @@ func testUserStoreGetSystemAdminProfiles(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("all system admin profiles", func(t *testing.T) { result := <-ss.User().GetSystemAdminProfiles() @@ -1215,6 +1286,13 @@ func testUserStoreGetByEmail(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get u1 by email", func(t *testing.T) { result := <-ss.User().GetByEmail(u1.Email) @@ -1276,6 +1354,13 @@ func testUserStoreGetByAuthData(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get by u1 auth", func(t *testing.T) { result := <-ss.User().GetByAuth(u1.AuthData, u1.AuthService) @@ -1333,6 +1418,13 @@ func testUserStoreGetByUsername(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get u1 by username", func(t *testing.T) { result := <-ss.User().GetByUsername(u1.Username) @@ -1397,6 +1489,13 @@ func testUserStoreGetForLogin(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() t.Run("get u1 by username, allow both", func(t *testing.T) { result := <-ss.User().GetForLogin(u1.Username, true, true) @@ -1666,6 +1765,13 @@ func testUserStoreGetRecentlyActiveUsersForTeam(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() millis := model.GetMillis() u3.LastActivityAt = millis @@ -1727,6 +1833,13 @@ func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) { })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() u4 := store.Must(ss.User().Save(&model.User{ Email: MakeEmail(), @@ -1830,6 +1943,13 @@ func testUserStoreSearch(t *testing.T, ss store.Store) { } store.Must(ss.User().Save(u3)) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() u5 := &model.User{ Username: "yu" + model.NewId(), @@ -2160,6 +2280,13 @@ func testUserStoreSearchNotInChannel(t *testing.T, ss store.Store) { } store.Must(ss.User().Save(u3)) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() tid := model.NewId() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)) @@ -2367,6 +2494,13 @@ func testUserStoreSearchInChannel(t *testing.T, ss store.Store) { } store.Must(ss.User().Save(u3)) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() tid := model.NewId() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)) @@ -2513,6 +2647,13 @@ func testUserStoreSearchNotInTeam(t *testing.T, ss store.Store) { } store.Must(ss.User().Save(u3)) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() u4 := &model.User{ Username: "simon" + model.NewId(), @@ -2685,6 +2826,13 @@ func testUserStoreSearchWithoutTeam(t *testing.T, ss store.Store) { } store.Must(ss.User().Save(u3)) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() tid := model.NewId() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)) @@ -2753,6 +2901,94 @@ func testUserStoreSearchWithoutTeam(t *testing.T, ss store.Store) { } } +func testCount(t *testing.T, ss store.Store) { + // Regular + teamId := model.NewId() + u1 := &model.User{} + u1.Email = MakeEmail() + store.Must(ss.User().Save(u1)) + defer func() { store.Must(ss.User().PermanentDelete(u1.Id)) }() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) + + // Deleted + u2 := &model.User{} + u2.Email = MakeEmail() + u2.DeleteAt = model.GetMillis() + store.Must(ss.User().Save(u2)) + defer func() { store.Must(ss.User().PermanentDelete(u2.Id)) }() + + // Bot + u3 := store.Must(ss.User().Save(&model.User{ + Email: MakeEmail(), + })).(*model.User) + defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() + + result := <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: false, + IncludeDeleted: false, + TeamId: "", + }) + require.Nil(t, result.Err) + require.Equal(t, int64(1), result.Data.(int64)) + + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: false, + TeamId: "", + }) + require.Nil(t, result.Err) + require.Equal(t, int64(2), result.Data.(int64)) + + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: false, + IncludeDeleted: true, + TeamId: "", + }) + require.Nil(t, result.Err) + require.Equal(t, int64(2), result.Data.(int64)) + + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: "", + }) + require.Nil(t, result.Err) + require.Equal(t, int64(3), result.Data.(int64)) + + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + ExcludeRegularUsers: true, + TeamId: "", + }) + require.Nil(t, result.Err) + require.Equal(t, int64(1), result.Data.(int64)) + + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: teamId, + }) + require.Nil(t, result.Err) + require.Equal(t, int64(1), result.Data.(int64)) + + result = <-ss.User().Count(model.UserCountOptions{ + IncludeBotAccounts: true, + IncludeDeleted: true, + TeamId: model.NewId(), + }) + require.Nil(t, result.Err) + require.Equal(t, int64(0), result.Data.(int64)) + +} + func testUserStoreAnalyticsGetInactiveUsersCount(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() @@ -2849,6 +3085,13 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { Username: "u3" + model.NewId(), })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u3.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + })) + u3.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u3.Id)) }() var etag1, etag2, etag3 string @@ -3030,6 +3273,13 @@ func testUserStoreGetAllAfter(t *testing.T, ss store.Store) { Username: "u2" + model.NewId(), })).(*model.User) defer func() { store.Must(ss.User().PermanentDelete(u2.Id)) }() + store.Must(ss.Bot().Save(&model.Bot{ + UserId: u2.Id, + Username: u2.Username, + OwnerId: u1.Id, + })) + u2.IsBot = true + defer func() { store.Must(ss.Bot().PermanentDelete(u2.Id)) }() expected := []*model.User{u1, u2} if strings.Compare(u2.Id, u1.Id) < 0 { diff --git a/vendor/github.com/mattermost/gorp/gorp.go b/vendor/github.com/mattermost/gorp/gorp.go index 784712fd9a..a83b255201 100644 --- a/vendor/github.com/mattermost/gorp/gorp.go +++ b/vendor/github.com/mattermost/gorp/gorp.go @@ -257,17 +257,15 @@ func columnToFieldIndex(m *DbMap, t reflect.Type, name string, cols []string) ([ cArguments := strings.Split(field.Tag.Get("db"), ",") fieldName = cArguments[0] - if fieldName == "-" { - return false - } else if fieldName == "" { - fieldName = field.Name - } if tableMapped { colMap := colMapOrNil(table, fieldName) if colMap != nil { fieldName = colMap.ColumnName } } + if fieldName == "" || fieldName == "-" { + fieldName = field.Name + } return colName == strings.ToLower(fieldName) }) if found { diff --git a/web/context.go b/web/context.go index 38557261ec..7464ae0a82 100644 --- a/web/context.go +++ b/web/context.go @@ -182,7 +182,7 @@ func NewInvalidUrlParamError(parameter string) *model.AppError { } func (c *Context) SetPermissionError(permission *model.Permission) { - c.Err = model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.App.Session.UserId+", "+"permission="+permission.Id, http.StatusForbidden) + c.Err = c.App.MakePermissionError(permission) } func (c *Context) SetSiteURLHeader(url string) { @@ -563,3 +563,14 @@ func (c *Context) RequireSyncableType() *Context { } return c } + +func (c *Context) RequireBotUserId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.BotUserId) != 26 { + c.SetInvalidUrlParam("bot_user_id") + } + return c +} diff --git a/web/params.go b/web/params.go index 286286c4d4..5f39edce8e 100644 --- a/web/params.go +++ b/web/params.go @@ -58,6 +58,7 @@ type Params struct { RemoteId string SyncableId string SyncableType model.GroupSyncableType + BotUserId string } func ParamsFromRequest(r *http.Request) *Params { @@ -226,5 +227,10 @@ func ParamsFromRequest(r *http.Request) *Params { params.SyncableType = model.GroupSyncableTypeChannel } } + + if val, ok := props["bot_user_id"]; ok { + params.BotUserId = val + } + return params }