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 <jesse.hallam@gmail.com>

* s/model.UserFromBotModel/model.UserFromBot/g

* Update model/bot.go

Co-Authored-By: lieut-data <jesse.hallam@gmail.com>

* Update model/client4.go

Co-Authored-By: lieut-data <jesse.hallam@gmail.com>

* 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 3fcecd521a.

* 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 <crspeller@gmail.com>

* 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 <crspeller@gmail.com>

* 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
Этот коммит содержится в:
Christopher Speller
2019-03-05 07:06:45 -08:00
коммит произвёл GitHub
родитель 80e0d01fe5
Коммит 06b579d18a
53 изменённых файлов: 5951 добавлений и 403 удалений

209
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 &copy
}
// 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)
}

666
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())
}
})
}
}

Просмотреть файл

@@ -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.

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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,
}
}

Просмотреть файл

@@ -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...,

Просмотреть файл

@@ -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 {

16
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
}