From 06b579d18aa3bb5d1aa960f4a3261c9d3d430cb4 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Tue, 5 Mar 2019 07:06:45 -0800 Subject: [PATCH] MM-12393 Server side of bot accounts. (#10378) * bots model, store and api (#9903) * bots model, store and api Fixes: MM-13100, MM-13101, MM-13103, MM-13105, MMM-13119 * uncomment tests incorrectly commented, and fix merge issues * add etags support * add missing licenses * remove unused sqlbuilder.go (for now...) * rejig permissions * split out READ_BOTS into READ_BOTS and READ_OTHERS_BOTS, the latter implicitly allowing the former * make MANAGE_OTHERS_BOTS imply MANAGE_BOTS * conform to general rest api pattern * eliminate redundant http.StatusOK * Update api4/bot.go Co-Authored-By: lieut-data * s/model.UserFromBotModel/model.UserFromBot/g * Update model/bot.go Co-Authored-By: lieut-data * Update model/client4.go Co-Authored-By: lieut-data * move sessionHasPermissionToManageBot to app/authorization.go * use api.ApiSessionRequired for createBot * introduce BOT_DESCRIPTION_MAX_RUNES constant * MM-13512 Prevent getting a user by email based on privacy settings (#10021) * MM-13512 Prevent getting a user by email based on privacy settings * Add additional config settings to tests * upgrade db to 5.7 (#10019) * MM-13526 Add validation when setting a user's Locale field (#10022) * Fix typos (#10024) * Fixing first user being created with system admin privilages without being explicity specified. (#10014) * Revert "Support for Embeded chat (#9129)" (#10017) This reverts commit 3fcecd521a5c6ccfdb52fb4c3fb1f8c6ea528a4e. * s/DisableBot/UpdateBotActive * add permissions on upgrade * Update NOTICE.txt (#10054) - add new dependency (text) - handle switch to forked dependency (go-gomail -> go-mail) - misc copyright owner updates * avoid leaking bot knowledge without permission * [GH-6798] added a new api endpoint to get the bulk reactions for posts (#10049) * 6798 added a new api to get the bulk reactions for posts * 6798 added the permsission check before getting the reactions * GH-6798 added a new app function for the new endpoint * 6798 added a store method to get reactions for multiple posts * 6798 connected the app function with the new store function * 6798 fixed the review comments * MM-13559 Update model.post.is_valid.file_ids.app_error text per report (#10055) Ticket: https://mattermost.atlassian.net/browse/MM-13559 Report: https://github.com/mattermost/mattermost-server/issues/10023 * Trigger Login Hooks with OAuth (#10061) * make BotStore.GetAll deterministic even on duplicate CreateAt * fix spurious TestMuteCommandSpecificChannel test failure See https://community-daily.mattermost.com/core/pl/px9p8s3dzbg1pf3ddrm5cr36uw * fix race in TestExportUserChannels * TestExportUserChannels: remove SaveMember call, as it is redundant and used to be silently failing anyway * MM-13117: bot tokens (#10111) * eliminate redundant Client/AdminClient declarations * harden TestUpdateChannelScheme to API failures * eliminate unnecessary config restoration * minor cleanup * make TestGenerateMfaSecret config dependency explicit * TestCreateUserAccessToken for bots * TestGetUserAccessToken* for bots * leverage SessionHasPermissionToUserOrBot for user token APIs * Test(Revoke|Disable|Enable)UserAccessToken * make EnableUserAccessTokens explicit, so as to not rely on local config.json * uncomment TestResetPassword, but still skip * mark assert(Invalid)Token as helper * fix whitespace issues * fix mangled comments * MM-13116: bot plugin api (#10113) * MM-13117: expose bot API to plugins This also changes the `CreatorId` column definition to allow for plugin ids, as the default unless the plugin overrides is to use the plugin id here. This branch hasn't hit master yet, so no migration needed. * gofmt issues * expunge use of BotList in plugin/client API * introduce model.BotGetOptions * use botUserId term for clarity * MM-13129 Adding functionality to deal with orphaned bots (#10238) * Add way to list orphaned bots. * Add /assign route to modify ownership of bot accounts. * Apply suggestions from code review Co-Authored-By: crspeller * MM-13120: add IsBot field to returned user objects (#10103) * MM-13104: forbid bot login (#10251) * MM-13104: disallow bot login * fix shadowing * MM-13136 Disable user bots when user is disabled. (#10293) * Disable user bots when user is disabled. * Grammer. Co-Authored-By: crspeller * Fixing bot branch for test changes. * Don't use external dependancies in bot plugin tests. * Rename bot CreatorId to OwnerId * Adding ability to re-enable bots * Fixing IsBot to not attempt to be saved to DB. * Adding diagnostics and licencing counting for bot accounts. * Modifying gorp to allow reading of '-' fields. * Removing unnessisary nil values from UserCountOptions. * Changing comment to GoDoc format * Improving user count SQL * Some improvments from feedback. * Omit empty on User.IsBot --- Gopkg.lock | 6 +- api4/api.go | 7 + api4/bot.go | 200 +++ api4/bot_test.go | 938 +++++++++++++ api4/user.go | 12 +- api4/user_test.go | 1192 +++++++++++++---- app/analytics.go | 6 +- app/app_test.go | 15 + app/authentication.go | 11 + app/authorization.go | 48 + app/bot.go | 180 +++ app/bot_test.go | 549 ++++++++ app/diagnostics.go | 14 +- app/import_functions_test.go | 30 +- app/license.go | 2 +- app/login.go | 5 + app/oauth.go | 7 + app/plugin_api.go | 32 + app/plugin_api_test.go | 145 ++ app/security_update_check.go | 4 +- app/user.go | 72 +- app/user_test.go | 54 +- i18n/en.json | 60 + model/bot.go | 209 +++ model/bot_test.go | 666 +++++++++ model/client4.go | 117 ++ model/config.go | 5 + model/permission.go | 40 + model/role.go | 5 + model/user.go | 1 + model/user_count.go | 16 + plugin/api.go | 30 + plugin/client_rpc_generated.go | 176 +++ plugin/plugintest/api.go | 141 ++ store/layered_store.go | 4 + store/sqlstore/bot_store.go | 253 ++++ store/sqlstore/bot_store_test.go | 14 + store/sqlstore/store.go | 1 + store/sqlstore/supplier.go | 7 + store/sqlstore/upgrade.go | 59 +- store/sqlstore/user_store.go | 60 +- store/store.go | 12 +- store/storetest/bot_store.go | 441 ++++++ store/storetest/mocks/BotStore.go | 94 ++ .../mocks/LayeredStoreDatabaseLayer.go | 16 + store/storetest/mocks/SqlStore.go | 16 + store/storetest/mocks/Store.go | 16 + store/storetest/mocks/UserStore.go | 48 +- store/storetest/store.go | 3 + store/storetest/user_store.go | 288 +++- vendor/github.com/mattermost/gorp/gorp.go | 8 +- web/context.go | 13 +- web/params.go | 6 + 53 files changed, 5951 insertions(+), 403 deletions(-) create mode 100644 api4/bot.go create mode 100644 api4/bot_test.go create mode 100644 app/bot.go create mode 100644 app/bot_test.go create mode 100644 model/bot.go create mode 100644 model/bot_test.go create mode 100644 model/user_count.go create mode 100644 store/sqlstore/bot_store.go create mode 100644 store/sqlstore/bot_store_test.go create mode 100644 store/storetest/bot_store.go create mode 100644 store/storetest/mocks/BotStore.go 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 }