Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

435
server/channels/api4/api.go Обычный файл
Просмотреть файл

@@ -0,0 +1,435 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/gorilla/mux"
graphql "github.com/graph-gophers/graphql-go"
_ "github.com/mattermost/go-i18n/i18n"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
type Routes struct {
Root *mux.Router // ''
APIRoot *mux.Router // 'api/v4'
APIRoot5 *mux.Router // 'api/v5'
Users *mux.Router // 'api/v4/users'
User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
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]+}'
TeamForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}'
UserThreads *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads'
UserThread *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/threads/{thread_id:[A-Za-z0-9]+}'
TeamByName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}'
TeamMembers *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members'
TeamMember *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
TeamMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/members'
Channels *mux.Router // 'api/v4/channels'
Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}'
ChannelForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/{channel_id:[A-Za-z0-9]+}'
ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelByNameForTeamName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels'
ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members'
ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/members'
ChannelModerations *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/moderations'
ChannelCategories *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams/{team_id:[A-Za-z0-9]+}/channels/categories'
Posts *mux.Router // 'api/v4/posts'
Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts'
PostsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts'
PostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}'
Files *mux.Router // 'api/v4/files'
File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
Uploads *mux.Router // 'api/v4/uploads'
Upload *mux.Router // 'api/v4/uploads/{upload_id:[A-Za-z0-9]+}'
Plugins *mux.Router // 'api/v4/plugins'
Plugin *mux.Router // 'api/v4/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}'
PublicFile *mux.Router // '/files/{file_id:[A-Za-z0-9]+}/public'
Commands *mux.Router // 'api/v4/commands'
Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}'
Hooks *mux.Router // 'api/v4/hooks'
IncomingHooks *mux.Router // 'api/v4/hooks/incoming'
IncomingHook *mux.Router // 'api/v4/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
OutgoingHooks *mux.Router // 'api/v4/hooks/outgoing'
OutgoingHook *mux.Router // 'api/v4/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
OAuth *mux.Router // 'api/v4/oauth'
OAuthApps *mux.Router // 'api/v4/oauth/apps'
OAuthApp *mux.Router // 'api/v4/oauth/apps/{app_id:[A-Za-z0-9]+}'
OpenGraph *mux.Router // 'api/v4/opengraph'
SAML *mux.Router // 'api/v4/saml'
Compliance *mux.Router // 'api/v4/compliance'
Cluster *mux.Router // 'api/v4/cluster'
Image *mux.Router // 'api/v4/image'
LDAP *mux.Router // 'api/v4/ldap'
Elasticsearch *mux.Router // 'api/v4/elasticsearch'
Bleve *mux.Router // 'api/v4/bleve'
DataRetention *mux.Router // 'api/v4/data_retention'
Brand *mux.Router // 'api/v4/brand'
System *mux.Router // 'api/v4/system'
Jobs *mux.Router // 'api/v4/jobs'
Preferences *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/preferences'
License *mux.Router // 'api/v4/license'
Public *mux.Router // 'api/v4/public'
Reactions *mux.Router // 'api/v4/reactions'
Roles *mux.Router // 'api/v4/roles'
Schemes *mux.Router // 'api/v4/schemes'
Emojis *mux.Router // 'api/v4/emoji'
Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
EmojiByName *mux.Router // 'api/v4/emoji/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
ReactionByNameForPostForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/posts/{post_id:[A-Za-z0-9]+}/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}'
TermsOfService *mux.Router // 'api/v4/terms_of_service'
Groups *mux.Router // 'api/v4/groups'
Cloud *mux.Router // 'api/v4/cloud'
Imports *mux.Router // 'api/v4/imports'
Exports *mux.Router // 'api/v4/exports'
Export *mux.Router // 'api/v4/exports/{export_name:.+\\.zip}'
RemoteCluster *mux.Router // 'api/v4/remotecluster'
SharedChannels *mux.Router // 'api/v4/sharedchannels'
Permissions *mux.Router // 'api/v4/permissions'
InsightsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/top'
InsightsForUser *mux.Router // 'api/v4/users/me/top'
Usage *mux.Router // 'api/v4/usage'
WorkTemplates *mux.Router // 'api/v4/worktemplates'
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
Drafts *mux.Router // 'api/v4/drafts'
}
type API struct {
srv *app.Server
schema *graphql.Schema
BaseRoutes *Routes
}
func Init(srv *app.Server) (*API, error) {
api := &API{
srv: srv,
BaseRoutes: &Routes{},
}
api.BaseRoutes.Root = srv.Router
api.BaseRoutes.APIRoot = srv.Router.PathPrefix(model.APIURLSuffix).Subrouter()
api.BaseRoutes.APIRoot5 = srv.Router.PathPrefix(model.APIURLSuffixV5).Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
api.BaseRoutes.User = api.BaseRoutes.APIRoot.PathPrefix("/users/{user_id:[A-Za-z0-9]+}").Subrouter()
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()
api.BaseRoutes.TeamForUser = api.BaseRoutes.TeamsForUser.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.UserThreads = api.BaseRoutes.TeamForUser.PathPrefix("/threads").Subrouter()
api.BaseRoutes.UserThread = api.BaseRoutes.TeamForUser.PathPrefix("/threads/{thread_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/members").Subrouter()
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelForUser = api.BaseRoutes.User.PathPrefix("/channels/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
api.BaseRoutes.ChannelModerations = api.BaseRoutes.Channel.PathPrefix("/moderations").Subrouter()
api.BaseRoutes.ChannelCategories = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/categories").Subrouter()
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
api.BaseRoutes.PostsForUser = api.BaseRoutes.User.PathPrefix("/posts").Subrouter()
api.BaseRoutes.PostForUser = api.BaseRoutes.PostsForUser.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Files = api.BaseRoutes.APIRoot.PathPrefix("/files").Subrouter()
api.BaseRoutes.File = api.BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PublicFile = api.BaseRoutes.Root.PathPrefix("/files/{file_id:[A-Za-z0-9]+}/public").Subrouter()
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
api.BaseRoutes.OAuth = api.BaseRoutes.APIRoot.PathPrefix("/oauth").Subrouter()
api.BaseRoutes.OAuthApps = api.BaseRoutes.OAuth.PathPrefix("/apps").Subrouter()
api.BaseRoutes.OAuthApp = api.BaseRoutes.OAuthApps.PathPrefix("/{app_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Compliance = api.BaseRoutes.APIRoot.PathPrefix("/compliance").Subrouter()
api.BaseRoutes.Cluster = api.BaseRoutes.APIRoot.PathPrefix("/cluster").Subrouter()
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
api.BaseRoutes.Brand = api.BaseRoutes.APIRoot.PathPrefix("/brand").Subrouter()
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
api.BaseRoutes.Preferences = api.BaseRoutes.User.PathPrefix("/preferences").Subrouter()
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
api.BaseRoutes.Public = api.BaseRoutes.APIRoot.PathPrefix("/public").Subrouter()
api.BaseRoutes.Reactions = api.BaseRoutes.APIRoot.PathPrefix("/reactions").Subrouter()
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
api.BaseRoutes.Elasticsearch = api.BaseRoutes.APIRoot.PathPrefix("/elasticsearch").Subrouter()
api.BaseRoutes.Bleve = api.BaseRoutes.APIRoot.PathPrefix("/bleve").Subrouter()
api.BaseRoutes.DataRetention = api.BaseRoutes.APIRoot.PathPrefix("/data_retention").Subrouter()
api.BaseRoutes.Emojis = api.BaseRoutes.APIRoot.PathPrefix("/emoji").Subrouter()
api.BaseRoutes.Emoji = api.BaseRoutes.APIRoot.PathPrefix("/emoji/{emoji_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.EmojiByName = api.BaseRoutes.Emojis.PathPrefix("/name/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
api.BaseRoutes.ReactionByNameForPostForUser = api.BaseRoutes.PostForUser.PathPrefix("/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
api.BaseRoutes.OpenGraph = api.BaseRoutes.APIRoot.PathPrefix("/opengraph").Subrouter()
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
api.BaseRoutes.Schemes = api.BaseRoutes.APIRoot.PathPrefix("/schemes").Subrouter()
api.BaseRoutes.Image = api.BaseRoutes.APIRoot.PathPrefix("/image").Subrouter()
api.BaseRoutes.TermsOfService = api.BaseRoutes.APIRoot.PathPrefix("/terms_of_service").Subrouter()
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
api.BaseRoutes.Cloud = api.BaseRoutes.APIRoot.PathPrefix("/cloud").Subrouter()
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
api.BaseRoutes.RemoteCluster = api.BaseRoutes.APIRoot.PathPrefix("/remotecluster").Subrouter()
api.BaseRoutes.SharedChannels = api.BaseRoutes.APIRoot.PathPrefix("/sharedchannels").Subrouter()
api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter()
api.BaseRoutes.InsightsForTeam = api.BaseRoutes.Team.PathPrefix("/top").Subrouter()
api.BaseRoutes.InsightsForUser = api.BaseRoutes.Users.PathPrefix("/me/top").Subrouter()
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
api.BaseRoutes.WorkTemplates = api.BaseRoutes.APIRoot.PathPrefix("/worktemplates").Subrouter()
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
api.BaseRoutes.Drafts = api.BaseRoutes.APIRoot.PathPrefix("/drafts").Subrouter()
api.InitUser()
api.InitBot()
api.InitTeam()
api.InitChannel()
api.InitPost()
api.InitFile()
api.InitUpload()
api.InitSystem()
api.InitLicense()
api.InitConfig()
api.InitWebhook()
api.InitPreference()
api.InitSaml()
api.InitCompliance()
api.InitCluster()
api.InitLdap()
api.InitElasticsearch()
api.InitBleve()
api.InitDataRetention()
api.InitBrand()
api.InitJob()
api.InitCommand()
api.InitStatus()
api.InitWebSocket()
api.InitEmoji()
api.InitOAuth()
api.InitReaction()
api.InitOpenGraph()
api.InitPlugin()
api.InitRole()
api.InitScheme()
api.InitImage()
api.InitTermsOfService()
api.InitGroup()
api.InitAction()
api.InitCloud()
api.InitImport()
api.InitRemoteCluster()
api.InitSharedChannels()
api.InitPermissions()
api.InitExport()
api.InitInsights()
api.InitUsage()
api.InitWorkTemplate()
api.InitHostedCustomer()
api.InitDrafts()
if err := api.InitGraphQL(); err != nil {
return nil, err
}
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
InitLocal(srv)
return api, nil
}
func InitLocal(srv *app.Server) *API {
api := &API{
srv: srv,
BaseRoutes: &Routes{},
}
api.BaseRoutes.Root = srv.LocalRouter
api.BaseRoutes.APIRoot = srv.LocalRouter.PathPrefix(model.APIURLSuffix).Subrouter()
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
api.BaseRoutes.User = api.BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
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.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter()
api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Channels = api.BaseRoutes.APIRoot.PathPrefix("/channels").Subrouter()
api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter()
api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter()
api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter()
api.BaseRoutes.Plugins = api.BaseRoutes.APIRoot.PathPrefix("/plugins").Subrouter()
api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
api.BaseRoutes.Commands = api.BaseRoutes.APIRoot.PathPrefix("/commands").Subrouter()
api.BaseRoutes.Command = api.BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Hooks = api.BaseRoutes.APIRoot.PathPrefix("/hooks").Subrouter()
api.BaseRoutes.IncomingHooks = api.BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
api.BaseRoutes.IncomingHook = api.BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.OutgoingHooks = api.BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
api.BaseRoutes.OutgoingHook = api.BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.License = api.BaseRoutes.APIRoot.PathPrefix("/license").Subrouter()
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/groups").Subrouter()
api.BaseRoutes.LDAP = api.BaseRoutes.APIRoot.PathPrefix("/ldap").Subrouter()
api.BaseRoutes.System = api.BaseRoutes.APIRoot.PathPrefix("/system").Subrouter()
api.BaseRoutes.Posts = api.BaseRoutes.APIRoot.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Post = api.BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.PostsForChannel = api.BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
api.BaseRoutes.Roles = api.BaseRoutes.APIRoot.PathPrefix("/roles").Subrouter()
api.BaseRoutes.Uploads = api.BaseRoutes.APIRoot.PathPrefix("/uploads").Subrouter()
api.BaseRoutes.Upload = api.BaseRoutes.Uploads.PathPrefix("/{upload_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.Imports = api.BaseRoutes.APIRoot.PathPrefix("/imports").Subrouter()
api.BaseRoutes.Exports = api.BaseRoutes.APIRoot.PathPrefix("/exports").Subrouter()
api.BaseRoutes.Export = api.BaseRoutes.Exports.PathPrefix("/{export_name:.+\\.zip}").Subrouter()
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
api.InitUserLocal()
api.InitTeamLocal()
api.InitChannelLocal()
api.InitConfigLocal()
api.InitWebhookLocal()
api.InitPluginLocal()
api.InitCommandLocal()
api.InitLicenseLocal()
api.InitBotLocal()
api.InitGroupLocal()
api.InitLdapLocal()
api.InitSystemLocal()
api.InitPostLocal()
api.InitRoleLocal()
api.InitUploadLocal()
api.InitImportLocal()
api.InitExportLocal()
api.InitJobLocal()
api.InitSamlLocal()
srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
return api
}
func (api *API) Handle404(w http.ResponseWriter, r *http.Request) {
app := app.New(app.ServerConnector(api.srv.Channels()))
web.Handle404(app, w, r)
}
var ReturnStatusOK = web.ReturnStatusOK

1341
server/channels/api4/apitestlib.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

39
server/channels/api4/bleve.go Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitBleve() {
api.BaseRoutes.Bleve.Handle("/purge_indexes", api.APISessionRequired(purgeBleveIndexes)).Methods("POST")
}
func purgeBleveIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("purgeBleveIndexes", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPurgeBleveIndexes) {
c.SetPermissionError(model.PermissionPurgeBleveIndexes)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("purgeBleveIndexes", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.PurgeBleveIndexes(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}

45
server/channels/api4/bleve_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestBlevePurgeIndexes(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("as system user", func(t *testing.T) {
resp, err := th.Client.PurgeBleveIndexes()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("as system user with write experimental permission", func(t *testing.T) {
th.AddPermissionToRole(model.PermissionPurgeBleveIndexes.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteExperimental.Id, model.SystemUserRoleId)
resp, err := th.Client.PurgeBleveIndexes()
require.NoError(t, err)
CheckOKStatus(t, resp)
})
t.Run("as system admin", func(t *testing.T) {
resp, err := th.SystemAdminClient.PurgeBleveIndexes()
require.NoError(t, err)
CheckOKStatus(t, resp)
})
t.Run("as restricted system admin", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
resp, err := th.SystemAdminClient.PurgeBleveIndexes()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}

320
server/channels/api4/bot.go Обычный файл
Просмотреть файл

@@ -0,0 +1,320 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
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("/convert_to_user", api.APISessionRequired(convertBotToUser)).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) {
var botPatch *model.BotPatch
err := json.NewDecoder(r.Body).Decode(&botPatch)
if err != nil {
c.SetInvalidParamWithErr("bot", err)
return
}
bot := &model.Bot{
OwnerId: c.AppContext.Session().UserId,
}
bot.Patch(botPatch)
auditRec := c.MakeAuditRecord("createBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "bot", bot)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateBot) {
c.SetPermissionError(model.PermissionCreateBot)
return
}
if user, err := c.App.GetUser(c.AppContext.Session().UserId); err == nil {
if user.IsBot {
c.SetPermissionError(model.PermissionCreateBot)
return
}
}
if !*c.App.Config().ServiceSettings.EnableBotAccountCreation {
c.Err = model.NewAppError("createBot", "api.bot.create_disabled", nil, "", http.StatusForbidden)
return
}
createdBot, appErr := c.App.CreateBot(c.AppContext, bot)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventObjectType("bot")
auditRec.AddEventResultState(createdBot) // overwrite meta
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(createdBot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func patchBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
var botPatch *model.BotPatch
err := json.NewDecoder(r.Body).Decode(&botPatch)
if err != nil {
c.SetInvalidParamWithErr("bot", err)
return
}
auditRec := c.MakeAuditRecord("patchBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameterAuditable(auditRec, "bot", botPatch)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
updatedBot, appErr := c.App.PatchBot(botUserId, botPatch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(updatedBot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(updatedBot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getBot(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
bot, appErr := c.App.GetBot(botUserId, includeDeleted)
if appErr != nil {
c.Err = appErr
return
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
// Allow access to any bot.
} else if bot.OwnerId == c.AppContext.Session().UserId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
// 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
}
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getBots(c *Context, w http.ResponseWriter, r *http.Request) {
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
onlyOrphaned, _ := strconv.ParseBool(r.URL.Query().Get("only_orphaned"))
var OwnerId string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOthersBots) {
// Get bots created by any user.
OwnerId = ""
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadBots) {
// Only get bots created by this user.
OwnerId = c.AppContext.Session().UserId
} else {
c.SetPermissionError(model.PermissionReadBots)
return
}
bots, appErr := c.App.GetBots(&model.BotGetOptions{
Page: c.Params.Page,
PerPage: c.Params.PerPage,
OwnerId: OwnerId,
IncludeDeleted: includeDeleted,
OnlyOrphaned: onlyOrphaned,
})
if appErr != nil {
c.Err = appErr
return
}
if c.HandleEtag(bots.Etag(), "Get Bots", w, r) {
return
}
if err := json.NewEncoder(w).Encode(bots); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func disableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
updateBotActive(c, w, false)
}
func enableBot(c *Context, w http.ResponseWriter, _ *http.Request) {
updateBotActive(c, w, true)
}
func updateBotActive(c *Context, w http.ResponseWriter, active bool) {
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
auditRec := c.MakeAuditRecord("updateBotActive", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameter(auditRec, "enable", active)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
bot, err := c.App.UpdateBotActive(c.AppContext, botUserId, active)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func assignBot(c *Context, w http.ResponseWriter, _ *http.Request) {
c.RequireUserId()
c.RequireBotUserId()
if c.Err != nil {
return
}
botUserId := c.Params.BotUserId
userId := c.Params.UserId
auditRec := c.MakeAuditRecord("assignBot", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "id", botUserId)
audit.AddEventParameter(auditRec, "user_id", userId)
if err := c.App.SessionHasPermissionToManageBot(*c.AppContext.Session(), botUserId); err != nil {
c.Err = err
return
}
if user, err := c.App.GetUser(userId); err == nil {
if user.IsBot {
c.SetPermissionError(model.PermissionAssignBot)
return
}
}
bot, err := c.App.UpdateBotOwner(botUserId, userId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(bot)
auditRec.AddEventObjectType("bot")
if err := json.NewEncoder(w).Encode(bot); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func convertBotToUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireBotUserId()
if c.Err != nil {
return
}
bot, err := c.App.GetBot(c.Params.BotUserId, false)
if err != nil {
c.Err = err
return
}
var userPatch model.UserPatch
jsonErr := json.NewDecoder(r.Body).Decode(&userPatch)
if jsonErr != nil || userPatch.Password == nil || *userPatch.Password == "" {
c.SetInvalidParamWithErr("userPatch", jsonErr)
return
}
systemAdmin, _ := strconv.ParseBool(r.URL.Query().Get("set_system_admin"))
auditRec := c.MakeAuditRecord("convertBotToUser", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "bot", bot)
audit.AddEventParameterAuditable(auditRec, "user_patch", &userPatch)
audit.AddEventParameter(auditRec, "set_system_admin", systemAdmin)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
user, err := c.App.ConvertBotToUser(c.AppContext, bot, &userPatch, systemAdmin)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(user)
auditRec.AddEventObjectType("user")
if err := json.NewEncoder(w).Encode(user); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}

15
server/channels/api4/bot_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitBotLocal() {
api.BaseRoutes.Bot.Handle("", api.APILocal(getBot)).Methods("GET")
api.BaseRoutes.Bot.Handle("", api.APILocal(patchBot)).Methods("PUT")
api.BaseRoutes.Bot.Handle("/disable", api.APILocal(disableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/enable", api.APILocal(enableBot)).Methods("POST")
api.BaseRoutes.Bot.Handle("/convert_to_user", api.APILocal(convertBotToUser)).Methods("POST")
api.BaseRoutes.Bot.Handle("/assign/{user_id:[A-Za-z0-9]+}", api.APILocal(assignBot)).Methods("POST")
api.BaseRoutes.Bots.Handle("", api.APILocal(getBots)).Methods("GET")
}

1359
server/channels/api4/bot_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

97
server/channels/api4/brand.go Обычный файл
Просмотреть файл

@@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitBrand() {
api.BaseRoutes.Brand.Handle("/image", api.APIHandlerTrustRequester(getBrandImage)).Methods("GET")
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage)).Methods("POST")
api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(deleteBrandImage)).Methods("DELETE")
}
func getBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
// No permission check required
img, err := c.App.GetBrandImage()
if err != nil {
w.WriteHeader(http.StatusNotFound)
w.Write(nil)
return
}
w.Header().Set("Content-Type", "image/png")
w.Write(img)
}
func uploadBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.parse.app_error", nil, "", http.StatusBadRequest)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) <= 0 {
c.Err = model.NewAppError("uploadBrandImage", "api.admin.upload_brand_image.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("uploadBrandImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
c.SetPermissionError(model.PermissionEditBrand)
return
}
if err := c.App.SaveBrandImage(imageArray[0]); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
w.WriteHeader(http.StatusCreated)
ReturnStatusOK(w)
}
func deleteBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("deleteBrandImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionEditBrand) {
c.SetPermissionError(model.PermissionEditBrand)
return
}
if err := c.App.DeleteBrandImage(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}

92
server/channels/api4/brand_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,92 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/testutils"
)
func TestGetBrandImage(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
_, resp, err := client.GetBrandImage()
require.Error(t, err)
CheckNotFoundStatus(t, resp)
client.Logout()
_, resp, err = client.GetBrandImage()
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, resp, err = th.SystemAdminClient.GetBrandImage()
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}
func TestUploadBrandImage(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
resp, err := client.UploadBrandImage(data)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// status code returns either forbidden or unauthorized
// note: forbidden is set as default at Client4.SetProfileImage when request is terminated early by server
client.Logout()
resp, err = client.UploadBrandImage(data)
require.Error(t, err)
if resp.StatusCode == http.StatusForbidden {
CheckForbiddenStatus(t, resp)
} else if resp.StatusCode == http.StatusUnauthorized {
CheckUnauthorizedStatus(t, resp)
} else {
require.Fail(t, "Should have failed either forbidden or unauthorized")
}
resp, err = th.SystemAdminClient.UploadBrandImage(data)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
}
func TestDeleteBrandImage(t *testing.T) {
th := Setup(t)
defer th.TearDown()
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
resp, err := th.SystemAdminClient.UploadBrandImage(data)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
resp, err = th.Client.DeleteBrandImage()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.Client.Logout()
resp, err = th.Client.DeleteBrandImage()
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
resp, err = th.SystemAdminClient.DeleteBrandImage()
require.NoError(t, err)
CheckOKStatus(t, resp)
resp, err = th.SystemAdminClient.DeleteBrandImage()
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}

2137
server/channels/api4/channel.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

333
server/channels/api4/channel_category.go Обычный файл
Просмотреть файл

@@ -0,0 +1,333 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func getCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
categories, appErr := c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, c.Params.UserId, c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(categoriesJSON)
}
func createCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("createCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoryCreateRequest model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoryCreateRequest)
if err != nil || c.Params.UserId != categoryCreateRequest.UserId || c.Params.TeamId != categoryCreateRequest.TeamId {
c.SetInvalidParamWithErr("category", err)
return
}
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryCreateRequest); appErr != nil {
c.Err = appErr
return
}
category, appErr := c.App.CreateSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, &categoryCreateRequest)
if appErr != nil {
c.Err = appErr
return
}
categoryJSON, err := json.Marshal(category)
if err != nil {
c.Err = model.NewAppError("createCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoryJSON)
}
func getCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
order, appErr := c.App.GetSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
err := json.NewEncoder(w).Encode(order)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
categoryOrder := model.ArrayFromJSON(r.Body)
for _, categoryId := range categoryOrder {
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, categoryId) {
c.SetInvalidParam("category")
return
}
}
err := c.App.UpdateSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId, categoryOrder)
if err != nil {
c.Err = err
return
}
auditRec.Success()
w.Write([]byte(model.ArrayToJSON(categoryOrder)))
}
func getCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
categories, appErr := c.App.GetSidebarCategory(c.AppContext, c.Params.CategoryId)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("getCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(categoriesJSON)
}
func updateCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoriesForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoriesUpdateRequest []*model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoriesUpdateRequest)
if err != nil {
c.SetInvalidParamWithErr("category", err)
return
}
for _, category := range categoriesUpdateRequest {
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, category.Id) {
c.SetInvalidParam("category")
return
}
}
if appErr := validateSidebarCategories(c, c.Params.TeamId, c.Params.UserId, categoriesUpdateRequest); appErr != nil {
c.Err = appErr
return
}
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, categoriesUpdateRequest)
if appErr != nil {
c.Err = appErr
return
}
categoriesJSON, err := json.Marshal(categories)
if err != nil {
c.Err = model.NewAppError("updateCategoriesForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoriesJSON)
}
func validateSidebarCategory(c *Context, teamId, userId string, category *model.SidebarCategoryWithChannels) *model.AppError {
channels, appErr := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
if appErr != nil {
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, "", http.StatusBadRequest).Wrap(appErr)
}
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
return nil
}
func validateSidebarCategories(c *Context, teamId, userId string, categories []*model.SidebarCategoryWithChannels) *model.AppError {
channels, err := c.App.GetChannelsForTeamForUser(c.AppContext, teamId, userId, &model.ChannelSearchOpts{
IncludeDeleted: true,
LastDeleteAt: 0,
})
if err != nil {
return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, err.Error(), http.StatusBadRequest)
}
for _, category := range categories {
category.Channels = validateSidebarCategoryChannels(c, userId, category.Channels, channels)
}
return nil
}
func validateSidebarCategoryChannels(c *Context, userId string, channelIds []string, channels model.ChannelList) []string {
var filtered []string
for _, channelId := range channelIds {
found := false
for _, channel := range channels {
if channel.Id == channelId {
found = true
break
}
}
if found {
filtered = append(filtered, channelId)
} else {
c.Logger.Info("Stopping user from adding channel to their sidebar when they are not a member", mlog.String("user_id", userId), mlog.String("channel_id", channelId))
}
}
return filtered
}
func updateCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("updateCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
var categoryUpdateRequest model.SidebarCategoryWithChannels
err := json.NewDecoder(r.Body).Decode(&categoryUpdateRequest)
if err != nil || categoryUpdateRequest.TeamId != c.Params.TeamId || categoryUpdateRequest.UserId != c.Params.UserId {
c.SetInvalidParamWithErr("category", err)
return
}
if appErr := validateSidebarCategory(c, c.Params.TeamId, c.Params.UserId, &categoryUpdateRequest); appErr != nil {
c.Err = appErr
return
}
categoryUpdateRequest.Id = c.Params.CategoryId
categories, appErr := c.App.UpdateSidebarCategories(c.AppContext, c.Params.UserId, c.Params.TeamId, []*model.SidebarCategoryWithChannels{&categoryUpdateRequest})
if appErr != nil {
c.Err = appErr
return
}
categoryJSON, err := json.Marshal(categories[0])
if err != nil {
c.Err = model.NewAppError("updateCategoryForTeamForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(categoryJSON)
}
func deleteCategoryForTeamForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireTeamId().RequireCategoryId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, c.Params.CategoryId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
auditRec := c.MakeAuditRecord("deleteCategoryForTeamForUser", audit.Fail)
defer c.LogAuditRec(auditRec)
appErr := c.App.DeleteSidebarCategory(c.AppContext, c.Params.UserId, c.Params.TeamId, c.Params.CategoryId)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
ReturnStatusOK(w)
}

568
server/channels/api4/channel_category_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,568 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestCreateCategoryForTeamForUser(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("should silently prevent the user from creating a category with an invalid channel ID", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
// Attempt to create the category
category := &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: user.Id,
TeamId: th.BasicTeam.Id,
DisplayName: "test",
},
Channels: []string{th.BasicChannel.Id, "notachannel", th.BasicChannel2.Id},
}
received, _, err := client.CreateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, category)
require.NoError(t, err)
assert.NotContains(t, received.Channels, "notachannel")
assert.Equal(t, []string{th.BasicChannel.Id, th.BasicChannel2.Id}, received.Channels)
})
t.Run("should silently prevent the user from creating a category with a channel that they're not a member of", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
// Have another user create a channel that user isn't a part of
channel, _, err := th.SystemAdminClient.CreateChannel(&model.Channel{
TeamId: th.BasicTeam.Id,
Type: model.ChannelTypeOpen,
Name: "testchannel",
})
require.NoError(t, err)
// Attempt to create the category
category := &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: user.Id,
TeamId: th.BasicTeam.Id,
DisplayName: "test",
},
Channels: []string{th.BasicChannel.Id, channel.Id},
}
received, _, err := client.CreateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, category)
require.NoError(t, err)
assert.NotContains(t, received.Channels, channel.Id)
assert.Equal(t, []string{th.BasicChannel.Id}, received.Channels)
})
t.Run("should return expected sort order value", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
customCategory, _, err := client.CreateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: user.Id,
TeamId: th.BasicTeam.Id,
DisplayName: "custom123",
},
})
require.NoError(t, err)
// Initial new category sort order is 10 (first)
require.Equal(t, int64(10), customCategory.SortOrder)
})
t.Run("should not crash with null input", func(t *testing.T) {
require.NotPanics(t, func() {
user, client := setupUserForSubtest(t, th)
payload := []byte(`null`)
route := fmt.Sprintf("/users/%s/teams/%s/channels/categories", user.Id, th.BasicTeam.Id)
r, err := client.DoAPIPostBytes(route, payload)
require.Error(t, err)
closeBody(r)
})
})
t.Run("should publish expected WS payload", func(t *testing.T) {
t.Skip("MM-42652")
userWSClient, err := th.CreateWebSocketClient()
require.NoError(t, err)
defer userWSClient.Close()
userWSClient.Listen()
category := &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
DisplayName: "test",
},
Channels: []string{th.BasicChannel.Id, "notachannel", th.BasicChannel2.Id},
}
received, _, err := th.Client.CreateSidebarCategoryForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, category)
require.NoError(t, err)
testCategories := []*model.SidebarCategoryWithChannels{
{
SidebarCategory: model.SidebarCategory{
Id: received.Id,
UserId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
Sorting: model.SidebarCategorySortRecent,
Muted: true,
},
Channels: []string{th.BasicChannel.Id},
},
}
testCategories, _, err = th.Client.UpdateSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, testCategories)
require.NoError(t, err)
b, err := json.Marshal(testCategories)
require.NoError(t, err)
expected := string(b)
var caught bool
func() {
for {
select {
case ev := <-userWSClient.EventChannel:
if ev.EventType() == model.WebsocketEventSidebarCategoryUpdated {
caught = true
data := ev.GetData()
updatedCategoriesData, ok := data["updatedCategories"]
require.True(t, ok)
require.EqualValues(t, expected, updatedCategoriesData)
}
case <-time.After(1 * time.Second):
return
}
}
}()
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventSidebarCategoryUpdated)
})
}
func TestUpdateCategoryForTeamForUser(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("should update the channel order of the Channels category", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
require.Len(t, channelsCategory.Channels, 5) // Town Square, Off Topic, and the 3 channels created by InitBasic
// Should return the correct values from the API
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channelsCategory.Channels[1], channelsCategory.Channels[0], channelsCategory.Channels[4], channelsCategory.Channels[3], channelsCategory.Channels[2]},
}
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory)
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received.Id)
assert.Equal(t, updatedCategory.Channels, received.Channels)
// And when requesting the category later
received, _, err = client.GetSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, "")
assert.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received.Id)
assert.Equal(t, updatedCategory.Channels, received.Channels)
})
t.Run("should update the sort order of the DM category", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
dmsCategory := categories.Categories[2]
require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type)
require.Equal(t, model.SidebarCategorySortRecent, dmsCategory.Sorting)
// Should return the correct values from the API
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: dmsCategory.SidebarCategory,
Channels: dmsCategory.Channels,
}
updatedCategory.Sorting = model.SidebarCategorySortAlphabetical
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, dmsCategory.Id, updatedCategory)
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, received.Id)
assert.Equal(t, model.SidebarCategorySortAlphabetical, received.Sorting)
// And when requesting the category later
received, _, err = client.GetSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, dmsCategory.Id, "")
assert.NoError(t, err)
assert.Equal(t, dmsCategory.Id, received.Id)
assert.Equal(t, model.SidebarCategorySortAlphabetical, received.Sorting)
})
t.Run("should update the display name of a custom category", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
customCategory, _, err := client.CreateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: user.Id,
TeamId: th.BasicTeam.Id,
DisplayName: "custom123",
},
})
require.NoError(t, err)
require.Equal(t, "custom123", customCategory.DisplayName)
// Should return the correct values from the API
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: customCategory.SidebarCategory,
Channels: customCategory.Channels,
}
updatedCategory.DisplayName = "abcCustom"
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, customCategory.Id, updatedCategory)
assert.NoError(t, err)
assert.Equal(t, customCategory.Id, received.Id)
assert.Equal(t, updatedCategory.DisplayName, received.DisplayName)
// And when requesting the category later
received, _, err = client.GetSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, customCategory.Id, "")
assert.NoError(t, err)
assert.Equal(t, customCategory.Id, received.Id)
assert.Equal(t, updatedCategory.DisplayName, received.DisplayName)
})
t.Run("should update the channel order of the category even if it contains archived channels", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
require.Len(t, channelsCategory.Channels, 5) // Town Square, Off Topic, and the 3 channels created by InitBasic
// Delete one of the channels
_, err = client.DeleteChannel(th.BasicChannel.Id)
require.NoError(t, err)
// Should still be able to reorder the channels
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: []string{channelsCategory.Channels[1], channelsCategory.Channels[0], channelsCategory.Channels[4], channelsCategory.Channels[3], channelsCategory.Channels[2]},
}
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory)
require.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received.Id)
assert.Equal(t, updatedCategory.Channels, received.Channels)
})
t.Run("should silently prevent the user from adding an invalid channel ID", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: append(channelsCategory.Channels, "notachannel"),
}
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory)
require.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received.Id)
assert.NotContains(t, received.Channels, "notachannel")
assert.Equal(t, channelsCategory.Channels, received.Channels)
})
t.Run("should silently prevent the user from adding a channel that they're not a member of", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Have another user create a channel that user isn't a part of
channel, _, err := th.SystemAdminClient.CreateChannel(&model.Channel{
TeamId: th.BasicTeam.Id,
Type: model.ChannelTypeOpen,
Name: "testchannel",
})
require.NoError(t, err)
// Attempt to update the category
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: append(channelsCategory.Channels, channel.Id),
}
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory)
require.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received.Id)
assert.NotContains(t, received.Channels, channel.Id)
assert.Equal(t, channelsCategory.Channels, received.Channels)
})
t.Run("muting a category should mute all of its channels", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
require.True(t, len(channelsCategory.Channels) > 0)
// Mute the category
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
Id: channelsCategory.Id,
UserId: user.Id,
TeamId: th.BasicTeam.Id,
Sorting: channelsCategory.Sorting,
Muted: true,
},
Channels: channelsCategory.Channels,
}
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory)
require.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received.Id)
assert.True(t, received.Muted)
// Check that the muted category was saved in the database
received, _, err = client.GetSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, "")
require.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received.Id)
assert.True(t, received.Muted)
// Confirm that the channels in the category were muted
member, _, err := client.GetChannelMember(channelsCategory.Channels[0], user.Id, "")
require.NoError(t, err)
assert.True(t, member.IsChannelMuted())
})
t.Run("should not be able to mute DM category", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
dmsCategory := categories.Categories[2]
require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type)
require.Len(t, dmsCategory.Channels, 0)
// Ensure a DM channel exists
dmChannel, _, err := client.CreateDirectChannel(user.Id, th.BasicUser.Id)
require.NoError(t, err)
// Attempt to mute the category
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
Id: dmsCategory.Id,
UserId: user.Id,
TeamId: th.BasicTeam.Id,
Sorting: dmsCategory.Sorting,
Muted: true,
},
Channels: []string{dmChannel.Id},
}
received, _, err := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, dmsCategory.Id, updatedCategory)
require.NoError(t, err)
assert.Equal(t, dmsCategory.Id, received.Id)
assert.False(t, received.Muted)
// Check that the muted category was not saved in the database
received, _, err = client.GetSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, dmsCategory.Id, "")
require.NoError(t, err)
assert.Equal(t, dmsCategory.Id, received.Id)
assert.False(t, received.Muted)
// Confirm that the channels in the category were not muted
member, _, err := client.GetChannelMember(dmChannel.Id, user.Id, "")
require.NoError(t, err)
assert.False(t, member.IsChannelMuted())
})
t.Run("should not crash with null input", func(t *testing.T) {
require.NotPanics(t, func() {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
dmsCategory := categories.Categories[2]
payload := []byte(`null`)
route := fmt.Sprintf("/users/%s/teams/%s/channels/categories/%s", user.Id, th.BasicTeam.Id, dmsCategory.Id)
r, err := client.DoAPIPutBytes(route, payload)
require.Error(t, err)
closeBody(r)
})
})
}
func TestUpdateCategoriesForTeamForUser(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("should silently prevent the user from adding an invalid channel ID", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: append(channelsCategory.Channels, "notachannel"),
}
received, _, err := client.UpdateSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{updatedCategory})
require.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received[0].Id)
assert.NotContains(t, received[0].Channels, "notachannel")
assert.Equal(t, channelsCategory.Channels, received[0].Channels)
})
t.Run("should silently prevent the user from adding a channel that they're not a member of", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// Have another user create a channel that user isn't a part of
channel, _, err := th.SystemAdminClient.CreateChannel(&model.Channel{
TeamId: th.BasicTeam.Id,
Type: model.ChannelTypeOpen,
Name: "testchannel",
})
require.NoError(t, err)
// Attempt to update the category
updatedCategory := &model.SidebarCategoryWithChannels{
SidebarCategory: channelsCategory.SidebarCategory,
Channels: append(channelsCategory.Channels, channel.Id),
}
received, _, err := client.UpdateSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{updatedCategory})
require.NoError(t, err)
assert.Equal(t, channelsCategory.Id, received[0].Id)
assert.NotContains(t, received[0].Channels, channel.Id)
assert.Equal(t, channelsCategory.Channels, received[0].Channels)
})
t.Run("should update order", func(t *testing.T) {
user, client := setupUserForSubtest(t, th)
categories, _, err := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory := categories.Categories[1]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
_, _, err = client.UpdateSidebarCategoryOrderForTeamForUser(user.Id, th.BasicTeam.Id, []string{categories.Order[1], categories.Order[0], categories.Order[2]})
require.NoError(t, err)
categories, _, err = client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.Len(t, categories.Categories, 3)
require.Len(t, categories.Order, 3)
channelsCategory = categories.Categories[0]
require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type)
// validate order
newOrder, _, err := client.GetSidebarCategoryOrderForTeamForUser(user.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
require.EqualValues(t, newOrder, categories.Order)
// try to update with missing category
_, _, err = client.UpdateSidebarCategoryOrderForTeamForUser(user.Id, th.BasicTeam.Id, []string{categories.Order[1], categories.Order[0]})
require.Error(t, err)
// try to update with invalid category
_, _, err = client.UpdateSidebarCategoryOrderForTeamForUser(user.Id, th.BasicTeam.Id, []string{categories.Order[1], categories.Order[0], "asd"})
require.Error(t, err)
})
}
func setupUserForSubtest(t *testing.T, th *TestHelper) (*model.User, *model.Client4) {
password := "password"
user, appErr := th.App.CreateUser(th.Context, &model.User{
Email: th.GenerateTestEmail(),
Username: "user_" + model.NewId(),
Password: password,
})
require.Nil(t, appErr)
th.LinkUserToTeam(user, th.BasicTeam)
th.AddUserToChannel(user, th.BasicChannel)
th.AddUserToChannel(user, th.BasicChannel2)
th.AddUserToChannel(user, th.BasicPrivateChannel)
client := th.CreateClient()
user, _, err := client.Login(user.Email, password)
require.NoError(t, err)
return user, client
}

446
server/channels/api4/channel_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,446 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitChannelLocal() {
api.BaseRoutes.Channels.Handle("", api.APILocal(getAllChannels)).Methods("GET")
api.BaseRoutes.Channels.Handle("", api.APILocal(localCreateChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("", api.APILocal(getChannel)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods("GET")
api.BaseRoutes.Channel.Handle("", api.APILocal(localDeleteChannel)).Methods("DELETE")
api.BaseRoutes.Channel.Handle("/patch", api.APILocal(localPatchChannel)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/move", api.APILocal(localMoveChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("/privacy", api.APILocal(localUpdateChannelPrivacy)).Methods("PUT")
api.BaseRoutes.Channel.Handle("/restore", api.APILocal(localRestoreChannel)).Methods("POST")
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(localRemoveChannelMember)).Methods("DELETE")
api.BaseRoutes.ChannelMember.Handle("", api.APILocal(getChannelMember)).Methods("GET")
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(localAddChannelMember)).Methods("POST")
api.BaseRoutes.ChannelMembers.Handle("", api.APILocal(getChannelMembers)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("", api.APILocal(getPublicChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APILocal(getDeletedChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelsForTeam.Handle("/private", api.APILocal(getPrivateChannelsForTeam)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.APILocal(getChannelByName)).Methods("GET")
api.BaseRoutes.ChannelByNameForTeamName.Handle("", api.APILocal(getChannelByNameForTeamName)).Methods("GET")
}
func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
var channel *model.Channel
err := json.NewDecoder(r.Body).Decode(&channel)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
auditRec := c.MakeAuditRecord("localCreateChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel", channel)
sc, appErr := c.App.CreateChannel(c.AppContext, channel, false)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(sc)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(sc); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localUpdateChannelPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
props := model.StringInterfaceFromJSON(r.Body)
privacy, ok := props["privacy"].(string)
if !ok || (model.ChannelType(privacy) != model.ChannelTypeOpen && model.ChannelType(privacy) != model.ChannelTypePrivate) {
c.SetInvalidParam("privacy")
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localUpdateChannelPrivacy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "privacy", privacy)
if channel.Name == model.DefaultChannelName && model.ChannelType(privacy) == model.ChannelTypePrivate {
c.Err = model.NewAppError("updateChannelPrivacy", "api.channel.update_channel_privacy.default_channel_error", nil, "", http.StatusBadRequest)
return
}
channel.Type = model.ChannelType(privacy)
updatedChannel, err := c.App.UpdateChannelPrivacy(c.AppContext, channel, nil)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + updatedChannel.Name)
if err := json.NewEncoder(w).Encode(updatedChannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRestoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localRestoreChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
channel, err = c.App.RestoreChannel(c.AppContext, channel, "")
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("name=" + channel.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localAddChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("localAddChannelMember", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
defer c.LogAuditRec(auditRec)
props := model.StringInterfaceFromJSON(r.Body)
userId, ok := props["user_id"].(string)
if !ok || !model.IsValidId(userId) {
c.SetInvalidParam("user_id")
return
}
audit.AddEventParameter(auditRec, "user_id", userId)
member := &model.ChannelMember{
ChannelId: c.Params.ChannelId,
UserId: userId,
}
postRootId, ok := props["post_root_id"].(string)
if ok && postRootId != "" && !model.IsValidId(postRootId) {
c.SetInvalidParam("post_root_id")
return
}
audit.AddEventParameter(auditRec, "post_root_id", postRootId)
if ok && len(postRootId) == 26 {
rootPost, err := c.App.GetSinglePost(postRootId, false)
if err != nil {
c.Err = err
return
}
if rootPost.ChannelId != member.ChannelId {
c.SetInvalidParam("post_root_id")
return
}
}
channel, err := c.App.GetChannel(c.AppContext, member.ChannelId)
if err != nil {
c.Err = err
return
}
audit.AddEventParameterAuditable(auditRec, "channel", channel)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() {
nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{member.UserId}, channel)
if err != nil {
if v, ok := err.(*model.AppError); ok {
c.Err = v
} else {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.error", nil, err.Error(), http.StatusBadRequest)
}
return
}
if len(nonMembers) > 0 {
c.Err = model.NewAppError("localAddChannelMember", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
return
}
}
cm, err := c.App.AddChannelMember(c.AppContext, member.UserId, channel, app.ChannelMemberOpts{
PostRootID: postRootId,
})
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("add_user_id", cm.UserId)
auditRec.AddEventResultState(cm)
auditRec.AddEventObjectType("channel_member")
c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(cm); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRemoveChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId().RequireUserId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if !(channel.Type == model.ChannelTypeOpen || channel.Type == model.ChannelTypePrivate) {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest)
return
}
if channel.IsGroupConstrained() && !user.IsBot {
c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("localRemoveChannelMember", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "remove_user_id", c.Params.UserId)
if err = c.App.RemoveUserFromChannel(c.AppContext, c.Params.UserId, "", channel); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("name=" + channel.Name + " user_id=" + c.Params.UserId)
ReturnStatusOK(w)
}
func localPatchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var patch *model.ChannelPatch
err := json.NewDecoder(r.Body).Decode(&patch)
if err != nil {
c.SetInvalidParamWithErr("channel", err)
return
}
originalOldChannel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
channel := originalOldChannel.DeepCopy()
auditRec := c.MakeAuditRecord("localPatchChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "channel_patch", patch)
channel.Patch(patch)
rchannel, appErr := c.App.UpdateChannel(c.AppContext, channel)
if appErr != nil {
c.Err = appErr
return
}
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("")
auditRec.AddEventResultState(rchannel)
auditRec.AddEventObjectType("channel")
if err := json.NewEncoder(w).Encode(rchannel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localMoveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
props := model.StringInterfaceFromJSON(r.Body)
teamId, ok := props["team_id"].(string)
if !ok {
c.SetInvalidParam("team_id")
return
}
force, ok := props["force"].(bool)
if !ok {
c.SetInvalidParam("force")
return
}
team, err := c.App.GetTeam(teamId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localMoveChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "team_id", teamId)
audit.AddEventParameter(auditRec, "force", force)
// TODO do we need these?
auditRec.AddMeta("channel_id", channel.Id)
auditRec.AddMeta("channel_name", channel.Name)
auditRec.AddMeta("team_id", team.Id)
auditRec.AddMeta("team_name", team.Name)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("moveChannel", "api.channel.move_channel.type.invalid", nil, "", http.StatusForbidden)
return
}
err = c.App.RemoveAllDeactivatedMembersFromChannel(c.AppContext, channel)
if err != nil {
c.Err = err
return
}
if force {
err = c.App.RemoveUsersFromChannelNotMemberOfTeam(c.AppContext, nil, channel, team)
if err != nil {
c.Err = err
return
}
}
err = c.App.MoveChannel(c.AppContext, team, channel, nil)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
auditRec.Success()
c.LogAudit("channel=" + channel.Name)
c.LogAudit("team=" + team.Name)
if err := json.NewEncoder(w).Encode(channel); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localDeleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("localDeleteChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
auditRec.AddEventPriorState(channel)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
c.Err = model.NewAppError("localDeleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest)
return
}
if c.Params.Permanent {
err = c.App.PermanentDeleteChannel(c.AppContext, channel)
} else {
err = c.App.DeleteChannel(c.AppContext, channel, "")
}
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(channel)
auditRec.AddEventObjectType("channel")
c.LogAudit("name=" + channel.Name)
ReturnStatusOK(w)
}

4624
server/channels/api4/channel_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

804
server/channels/api4/cloud.go Обычный файл
Просмотреть файл

@@ -0,0 +1,804 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/binary"
"encoding/json"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
func (api *API) InitCloud() {
// GET /api/v4/cloud/products
api.BaseRoutes.Cloud.Handle("/products", api.APISessionRequired(getCloudProducts)).Methods("GET")
// GET /api/v4/cloud/limits
api.BaseRoutes.Cloud.Handle("/limits", api.APISessionRequired(getCloudLimits)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/products/selfhosted", api.APISessionRequired(getSelfHostedProducts)).Methods("GET")
// POST /api/v4/cloud/payment
// POST /api/v4/cloud/payment/confirm
api.BaseRoutes.Cloud.Handle("/payment", api.APISessionRequired(createCustomerPayment)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/payment/confirm", api.APISessionRequired(confirmCustomerPayment)).Methods("POST")
// GET /api/v4/cloud/customer
// PUT /api/v4/cloud/customer
// PUT /api/v4/cloud/customer/address
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(getCloudCustomer)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/customer", api.APISessionRequired(updateCloudCustomer)).Methods("PUT")
api.BaseRoutes.Cloud.Handle("/customer/address", api.APISessionRequired(updateCloudCustomerAddress)).Methods("PUT")
// GET /api/v4/cloud/subscription
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription/self-serve-status", api.APISessionRequired(getLicenseSelfServeStatus)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT")
// GET /api/v4/cloud/request-trial
api.BaseRoutes.Cloud.Handle("/request-trial", api.APISessionRequired(requestCloudTrial)).Methods("PUT")
// GET /api/v4/cloud/validate-business-email
api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods("POST")
api.BaseRoutes.Cloud.Handle("/validate-workspace-business-email", api.APISessionRequired(validateWorkspaceBusinessEmail)).Methods("POST")
// POST /api/v4/cloud/webhook
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
// GET /api/v4/cloud/cws-health-check
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
api.BaseRoutes.Cloud.Handle("/delete-workspace", api.APISessionRequired(selfServeDeleteWorkspace)).Methods(http.MethodDelete)
}
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
subscription, err := c.App.Cloud().GetSubscription(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// if it is an end user, return basic subscription data without sensitive information
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
subscription = &model.Subscription{
ID: subscription.ID,
ProductID: subscription.ProductID,
IsFreeTrial: subscription.IsFreeTrial,
TrialEndAt: subscription.TrialEndAt,
CustomerID: "",
AddOns: []string{},
StartAt: 0,
EndAt: 0,
CreateAt: 0,
Seats: 0,
Status: "",
DNS: "",
LastInvoice: &model.Invoice{},
DelinquentSince: subscription.DelinquentSince,
}
}
json, err := json.Marshal(subscription)
if err != nil {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
userId := c.AppContext.Session().UserId
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.license_error", nil, "", http.StatusInternalServerError)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var subscriptionChange *model.SubscriptionChange
if err = json.Unmarshal(bodyBytes, &subscriptionChange); err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
currentSubscription, appErr := c.App.Cloud().GetSubscription(userId)
if appErr != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
changedSub, err := c.App.Cloud().ChangeSubscription(userId, currentSubscription.ID, subscriptionChange)
if err != nil {
appErr := model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
if err.Error() == "compliance-failed" {
c.Logger.Error("Compliance check failed", mlog.Err(err))
appErr.StatusCode = http.StatusUnprocessableEntity
}
c.Err = appErr
return
}
if subscriptionChange.Feedback != nil {
c.App.Srv().GetTelemetryService().SendTelemetry("downgrade_feedback", subscriptionChange.Feedback.ToMap())
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.changeSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
product, err := c.App.Cloud().GetCloudProduct(c.AppContext.Session().UserId, subscriptionChange.ProductID)
if err != nil || product == nil {
c.Logger.Error("Error finding the new cloud product", mlog.Err(err))
}
if product.SKU == string(model.SkuCloudStarter) {
w.Write(json)
return
}
isYearly := product.IsYearly()
// Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them
// At this point, the upgrade is complete.
if appErr := c.App.SendUpgradeConfirmationEmail(isYearly); appErr != nil {
c.Logger.Error("Error sending purchase confirmation email", mlog.Err(appErr))
}
w.Write(json)
}
func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
// check if the email needs to be set
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
// this value will not be empty when both emails (user admin and CWS customer) are not business email and
// a new business email was provided via the request business email modal
var startTrialRequest *model.StartCloudTrialRequest
if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, startTrialRequest.SubscriptionID, startTrialRequest.Email)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(changedSub)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
defer c.App.Srv().Cloud.InvalidateCaches()
w.Write(json)
}
func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var emailToValidate *model.ValidateBusinessEmailRequest
err = json.Unmarshal(bodyBytes, &emailToValidate)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
err = c.App.Cloud().ValidateBusinessEmail(user.Id, emailToValidate.Email)
if err != nil {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(err)
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
return
}
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
// get the cloud customer email to validate if is a valid business email
cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id)
if err != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusBadRequest)
return
}
emailErr := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email)
// if the current workspace email is not a valid business email
if emailErr != nil {
// grab the current admin email and validate it
errValidatingAdminEmail := c.App.Cloud().ValidateBusinessEmail(user.Id, user.Email)
if errValidatingAdminEmail != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, errValidatingAdminEmail.Error(), http.StatusForbidden)
emailResp := model.ValidateBusinessEmailResponse{IsValid: false}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
return
}
}
// if any of the emails is valid, return ok
emailResp := model.ValidateBusinessEmailResponse{IsValid: true}
if err := json.NewEncoder(w).Encode(emailResp); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getSelfHostedProducts(c *Context, w http.ResponseWriter, r *http.Request) {
products, err := c.App.Cloud().GetSelfHostedProducts(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteProductsData, err := json.Marshal(products)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
sanitizedProducts := []model.UserFacingProduct{}
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(byteSanitizedProductsData)
return
}
w.Write(byteProductsData)
}
func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
includeLegacyProducts := r.URL.Query().Get("include_legacy") == "true"
products, err := c.App.Cloud().GetCloudProducts(c.AppContext.Session().UserId, includeLegacyProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteProductsData, err := json.Marshal(products)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
sanitizedProducts := []model.UserFacingProduct{}
err = json.Unmarshal(byteProductsData, &sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
byteSanitizedProductsData, err := json.Marshal(sanitizedProducts)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(byteSanitizedProductsData)
return
}
w.Write(byteProductsData)
}
func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(limits)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
customer, err := c.App.Cloud().GetCloudCustomer(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
_, token, err := c.App.Srv().GenerateLicenseRenewalLink()
if err != nil {
c.Err = err
return
}
status, cloudErr := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if cloudErr != nil {
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr)
return
}
json, jsonErr := json.Marshal(status)
if jsonErr != nil {
c.Err = model.NewAppError("Api4.getLicenseSelfServeStatus", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
w.Write(json)
}
func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var customerInfo *model.CloudCustomerInfo
if err = json.Unmarshal(bodyBytes, &customerInfo); err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
customer, appErr := c.App.Cloud().UpdateCloudCustomer(c.AppContext.Session().UserId, customerInfo)
if appErr != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var address *model.Address
if err = json.Unmarshal(bodyBytes, &address); err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
customer, appErr := c.App.Cloud().UpdateCloudCustomerAddress(c.AppContext.Session().UserId, address)
if appErr != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(customer)
if err != nil {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func createCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("createCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
intent, err := c.App.Cloud().CreateCustomerPayment(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(intent)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(json)
}
func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
auditRec := c.MakeAuditRecord("confirmCustomerPayment", audit.Fail)
defer c.LogAuditRec(auditRec)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
var confirmRequest *model.ConfirmPaymentMethodRequest
if err = json.Unmarshal(bodyBytes, &confirmRequest); err != nil {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
err = c.App.Cloud().ConfirmCustomerPayment(c.AppContext.Session().UserId, confirmRequest)
if err != nil {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
invoices, appErr := c.App.Cloud().GetInvoicesForSubscription(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
return
}
json, err := json.Marshal(invoices)
if err != nil {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
c.RequireInvoiceId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) {
c.SetPermissionError(model.PermissionSysconsoleReadBilling)
return
}
pdfData, filename, appErr := c.App.Cloud().GetInvoicePDF(c.AppContext.Session().UserId, c.Params.InvoiceId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
return
}
web.WriteFileResponse(
filename,
"application/pdf",
int64(binary.Size(pdfData)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
bytes.NewReader(pdfData),
false,
w,
r,
)
}
func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
defer r.Body.Close()
var event *model.CWSWebhookPayload
if err = json.Unmarshal(bodyBytes, &event); err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
switch event.Event {
case model.EventTypeFailedPayment:
if nErr := c.App.SendPaymentFailedEmail(event.FailedPayment); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeFailedPaymentNoCard:
if nErr := c.App.SendNoCardPaymentFailedEmail(); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendUpgradeConfirmationEmail:
// isYearly determines whether to send the yearly or monthly Upgrade email
isYearly := false
if event.Subscription != nil && event.CloudWorkspaceOwner != nil {
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
// Get the current cloud product to determine whether it's a monthly or yearly product
product, err := c.App.Cloud().GetCloudProduct(user.Id, event.Subscription.ProductID)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
isYearly = product.IsYearly()
}
if nErr := c.App.SendUpgradeConfirmationEmail(isYearly); nErr != nil {
c.Err = nErr
return
}
case model.EventTypeSendAdminWelcomeEmail:
user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName)
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
teams, appErr := c.App.GetAllTeams()
if appErr != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode)
return
}
team := teams[0]
subscription, err := c.App.Cloud().GetSubscription(user.Id)
if err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if err := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); err != nil {
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError)
return
}
case model.EventTypeTriggerDelinquencyEmail:
var emailToTrigger model.DelinquencyEmail
if event.DelinquencyEmail != nil {
emailToTrigger = model.DelinquencyEmail(event.DelinquencyEmail.EmailToTrigger)
} else {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.delinquency_email.missing_email_to_trigger", nil, "", http.StatusInternalServerError)
return
}
if nErr := c.App.SendDelinquencyEmail(emailToTrigger); nErr != nil {
c.Err = nErr
return
}
default:
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
return
}
ReturnStatusOK(w)
}
func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
return
}
if err := cloud.CheckCWSConnection(c.AppContext.Session().UserId); err != nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.health_check.app_error", nil, "CWS Server is not available.", http.StatusInternalServerError)
return
}
ReturnStatusOK(w)
}
func selfServeDeleteWorkspace(c *Context, w http.ResponseWriter, r *http.Request) {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
var deleteRequest *model.WorkspaceDeletionRequest
if err = json.Unmarshal(bodyBytes, &deleteRequest); err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if err := c.App.Cloud().SelfServeDeleteWorkspace(c.AppContext.Session().UserId, deleteRequest); err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.server.cws.delete_workspace.app_error", nil, "CWS Server failed to delete workspace.", http.StatusInternalServerError)
return
}
c.App.Srv().GetTelemetryService().SendTelemetry("delete_workspace_feedback", deleteRequest.Feedback.ToMap())
ReturnStatusOK(w)
}

809
server/channels/api4/cloud_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,809 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces/mocks"
)
func Test_getCloudLimits(t *testing.T) {
t.Run("no license returns not implemented", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().RemoveLicense()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits()
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusForbidden, r.StatusCode, "Expected 403 forbidden")
})
t.Run("non cloud license returns not implemented", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense())
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits()
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusForbidden, r.StatusCode, "Expected 403 forbidden")
})
t.Run("error fetching limits returns internal server error", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := &mocks.CloudInterface{}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits()
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusInternalServerError, r.StatusCode, "Expected 500 Internal Server Error")
})
t.Run("unauthenticated users can not access", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Logout()
limits, r, err := th.Client.GetProductLimits()
require.Error(t, err)
require.Nil(t, limits)
require.Equal(t, http.StatusUnauthorized, r.StatusCode, "Expected 401 Unauthorized")
})
t.Run("good request with cloud server", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := &mocks.CloudInterface{}
ten := 10
mockLimits := &model.ProductLimits{
Messages: &model.MessagesLimits{
History: &ten,
},
}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(mockLimits, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
limits, r, err := th.Client.GetProductLimits()
require.NoError(t, err)
require.Equal(t, http.StatusOK, r.StatusCode, "Expected 200 OK")
require.Equal(t, mockLimits, limits)
require.Equal(t, *mockLimits.Messages.History, *limits.Messages.History)
})
}
func Test_GetSubscription(t *testing.T) {
deliquencySince := int64(2000000000)
subscription := &model.Subscription{
ID: "MySubscriptionID",
CustomerID: "MyCustomer",
ProductID: "SomeProductId",
AddOns: []string{},
StartAt: 1000000000,
EndAt: 2000000000,
CreateAt: 1000000000,
Seats: 10,
IsFreeTrial: "true",
DNS: "some.dns.server",
TrialEndAt: 2000000000,
LastInvoice: &model.Invoice{},
DelinquentSince: &deliquencySince,
}
userFacingSubscription := &model.Subscription{
ID: "MySubscriptionID",
CustomerID: "",
ProductID: "SomeProductId",
AddOns: []string{},
StartAt: 0,
EndAt: 0,
CreateAt: 0,
Seats: 0,
IsFreeTrial: "true",
DNS: "",
TrialEndAt: 2000000000,
LastInvoice: &model.Invoice{},
DelinquentSince: &deliquencySince,
}
t.Run("NON Admin users receive the user facing subscription", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionReturned, r, err := th.Client.GetSubscription()
require.NoError(t, err)
require.Equal(t, subscriptionReturned, userFacingSubscription)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
})
t.Run("Admin users receive the full subscription information", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionReturned, r, err := th.SystemAdminClient.GetSubscription()
require.NoError(t, err)
require.Equal(t, subscriptionReturned, subscription)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
})
}
func Test_requestTrial(t *testing.T) {
subscription := &model.Subscription{
ID: "MySubscriptionID",
CustomerID: "MyCustomer",
ProductID: "SomeProductId",
AddOns: []string{},
StartAt: 1000000000,
EndAt: 2000000000,
CreateAt: 1000000000,
Seats: 10,
DNS: "some.dns.server",
}
newValidBusinessEmail := model.StartCloudTrialRequest{Email: ""}
t.Run("NON Admin users are UNABLE to request the trial", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil)
cloud.Mock.On("InvalidateCaches").Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionChanged, r, err := th.Client.RequestCloudTrial(&newValidBusinessEmail)
require.Error(t, err)
require.Nil(t, subscriptionChanged)
require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden")
})
t.Run("ADMIN user are ABLE to request the trial", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil)
cloud.Mock.On("InvalidateCaches").Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(&newValidBusinessEmail)
require.NoError(t, err)
require.Equal(t, subscriptionChanged, subscription)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
})
t.Run("ADMIN user are ABLE to request the trial with valid business email", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
// patch the customer with the additional contact updated with the valid business email
newValidBusinessEmail.Email = *model.NewString("valid.email@mattermost.com")
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "valid.email@mattermost.com").Return(subscription, nil)
cloud.Mock.On("InvalidateCaches").Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(&newValidBusinessEmail)
require.NoError(t, err)
require.Equal(t, subscriptionChanged, subscription)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
})
t.Run("Empty body returns bad request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
r, err := th.SystemAdminClient.DoAPIPutBytes("/cloud/request-trial", nil)
require.Error(t, err)
closeBody(r)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
})
}
func Test_validateBusinessEmail(t *testing.T) {
t.Run("Returns forbidden for non admin executors", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
invalidEmail := model.ValidateBusinessEmailRequest{Email: "invalid@gmail.com"}
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, invalidEmail.Email).Return(errors.New("invalid email"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
res, err := th.Client.ValidateBusinessEmail(&invalidEmail)
require.Error(t, err)
require.Equal(t, http.StatusForbidden, res.StatusCode, "403")
})
t.Run("Returns forbidden for invalid business email", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
validBusinessEmail := model.ValidateBusinessEmailRequest{Email: "invalid@slacker.com"}
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, validBusinessEmail.Email).Return(errors.New("invalid email"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
res, err := th.SystemAdminClient.ValidateBusinessEmail(&validBusinessEmail)
require.Error(t, err)
require.Equal(t, http.StatusForbidden, res.StatusCode, "403")
})
t.Run("Validate business email for admin", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
validBusinessEmail := model.ValidateBusinessEmailRequest{Email: "valid@mattermost.com"}
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, validBusinessEmail.Email).Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
res, err := th.SystemAdminClient.ValidateBusinessEmail(&validBusinessEmail)
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode, "200")
})
t.Run("Empty body returns bad request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-business-email", nil)
require.Error(t, err)
closeBody(r)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
})
}
func Test_validateWorkspaceBusinessEmail(t *testing.T) {
t.Run("validate the Cloud Customer has used a valid email to create the workspace", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloudCustomerInfo := model.CloudCustomerInfo{
Email: "valid@mattermost.com",
}
cloudCustomer := &model.CloudCustomer{
CloudCustomerInfo: cloudCustomerInfo,
}
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(cloudCustomer, nil)
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
_, err := th.SystemAdminClient.ValidateWorkspaceBusinessEmail()
require.NoError(t, err)
})
t.Run("validate the Cloud Customer has used a invalid email to create the workspace and must validate admin email", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloudCustomerInfo := model.CloudCustomerInfo{
Email: "invalid@gmail.com",
}
cloudCustomer := &model.CloudCustomer{
CloudCustomerInfo: cloudCustomerInfo,
}
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(cloudCustomer, nil)
// first call to validate the cloud customer email
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(errors.New("invalid email"))
// second call to validate the user admin email
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, th.SystemAdminUser.Email).Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
_, err := th.SystemAdminClient.ValidateWorkspaceBusinessEmail()
require.NoError(t, err)
})
t.Run("Error while grabbing the cloud customer returns bad request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloudCustomerInfo := model.CloudCustomerInfo{
Email: "badrequest@gmail.com",
}
// return an error while getting the cloud customer so we validate the forbidden error return
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(nil, errors.New("error while gettings the cloud customer"))
// required cloud mocks so the request doesn't fail
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(errors.New("invalid email"))
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, th.SystemAdminUser.Email).Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-workspace-business-email", nil)
require.Error(t, err)
closeBody(r)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
})
}
func TestGetCloudProducts(t *testing.T) {
cloudProducts := []*model.Product{
{
ID: "prod_test1",
Name: "name",
Description: "description",
PricePerSeat: 10,
SKU: "sku",
PriceID: "price_id",
Family: "family",
RecurringInterval: "monthly",
BillingScheme: "billing_scheme",
CrossSellsTo: "",
},
{
ID: "prod_test2",
Name: "name2",
Description: "description2",
PricePerSeat: 100,
SKU: "sku2",
PriceID: "price_id2",
Family: "family2",
RecurringInterval: "monthly",
BillingScheme: "billing_scheme2",
CrossSellsTo: "prod_test3",
},
{
ID: "prod_test3",
Name: "name3",
Description: "description3",
PricePerSeat: 1000,
SKU: "sku3",
PriceID: "price_id3",
Family: "family3",
RecurringInterval: "yearly",
BillingScheme: "billing_scheme3",
CrossSellsTo: "prod_test2",
},
}
sanitizedProducts := []*model.Product{
{
ID: "prod_test1",
Name: "name",
PricePerSeat: 10,
SKU: "sku",
RecurringInterval: "monthly",
CrossSellsTo: "",
},
{
ID: "prod_test2",
Name: "name2",
PricePerSeat: 100,
SKU: "sku2",
RecurringInterval: "monthly",
CrossSellsTo: "prod_test3",
},
{
ID: "prod_test3",
Name: "name3",
PricePerSeat: 1000,
SKU: "sku3",
RecurringInterval: "yearly",
CrossSellsTo: "prod_test2",
},
}
t.Run("get products for admins", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetCloudProducts", mock.Anything, mock.Anything).Return(cloudProducts, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
returnedProducts, r, err := th.Client.GetCloudProducts()
require.NoError(t, err)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
require.Equal(t, returnedProducts, cloudProducts)
})
t.Run("get products for non admins", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetCloudProducts", mock.Anything, mock.Anything).Return(cloudProducts, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
returnedProducts, r, err := th.Client.GetCloudProducts()
require.NoError(t, err)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
require.Equal(t, returnedProducts, sanitizedProducts)
// make a more explicit check
require.Equal(t, returnedProducts[0].ID, "prod_test1")
require.Equal(t, returnedProducts[0].Name, "name")
require.Equal(t, returnedProducts[0].SKU, "sku")
require.Equal(t, returnedProducts[0].PricePerSeat, float64(10))
require.Equal(t, returnedProducts[0].Description, "")
require.Equal(t, returnedProducts[0].PriceID, "")
require.Equal(t, returnedProducts[0].Family, model.SubscriptionFamily(""))
require.Equal(t, returnedProducts[0].RecurringInterval, model.RecurringInterval("monthly"))
require.Equal(t, returnedProducts[0].BillingScheme, model.BillingScheme(""))
require.Equal(t, returnedProducts[0].CrossSellsTo, "")
require.Equal(t, returnedProducts[1].ID, "prod_test2")
require.Equal(t, returnedProducts[1].Name, "name2")
require.Equal(t, returnedProducts[1].SKU, "sku2")
require.Equal(t, returnedProducts[1].PricePerSeat, float64(100))
require.Equal(t, returnedProducts[1].Description, "")
require.Equal(t, returnedProducts[1].PriceID, "")
require.Equal(t, returnedProducts[1].Family, model.SubscriptionFamily(""))
require.Equal(t, returnedProducts[1].RecurringInterval, model.RecurringInterval("monthly"))
require.Equal(t, returnedProducts[1].BillingScheme, model.BillingScheme(""))
require.Equal(t, returnedProducts[1].CrossSellsTo, "prod_test3")
require.Equal(t, returnedProducts[2].ID, "prod_test3")
require.Equal(t, returnedProducts[2].Name, "name3")
require.Equal(t, returnedProducts[2].SKU, "sku3")
require.Equal(t, returnedProducts[2].PricePerSeat, float64(1000))
require.Equal(t, returnedProducts[2].Description, "")
require.Equal(t, returnedProducts[2].PriceID, "")
require.Equal(t, returnedProducts[2].Family, model.SubscriptionFamily(""))
require.Equal(t, returnedProducts[2].RecurringInterval, model.RecurringInterval("yearly"))
require.Equal(t, returnedProducts[2].BillingScheme, model.BillingScheme(""))
require.Equal(t, returnedProducts[2].CrossSellsTo, "prod_test2")
})
}
func Test_GetExpandStatsForSubscription(t *testing.T) {
status := &model.SubscriptionLicenseSelfServeStatusResponse{
IsExpandable: true,
}
licenseId := "licenseID"
t.Run("NON Admin users are UNABLE to request expand stats for the subscription", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
checksMade, r, err := th.Client.GetSubscriptionStatus(licenseId)
require.Error(t, err)
require.Nil(t, checksMade)
require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden")
})
t.Run("Admin users are UNABLE to request licenses is expendable due missing the id", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetLicenseSelfServeStatus", mock.Anything).Return(status, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
checks, r, err := th.Client.GetSubscriptionStatus("")
require.Error(t, err)
require.Nil(t, checks)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "400 Bad Request")
})
}
func TestGetSelfHostedProducts(t *testing.T) {
products := []*model.Product{
{
ID: "prod_test",
Name: "Self-Hosted Professional",
Description: "Ideal for small companies and departments with data security requirements",
PricePerSeat: 10,
SKU: "professional",
PriceID: "price_1JPXbNI67GP2qpb4VuFdFbwQ",
Family: "on-prem",
RecurringInterval: model.RecurringIntervalYearly,
},
{
ID: "prod_test2",
Name: "Self-Hosted Enterprise",
Description: "Built to scale for high-trust organizations and companies in regulated industries.",
PricePerSeat: 30,
SKU: "enterprise",
PriceID: "price_1JPXaVI67GP2qpb4l40bXyRu",
Family: "on-prem",
RecurringInterval: model.RecurringIntervalYearly,
},
}
sanitizedProducts := []*model.Product{
{
ID: "prod_test",
Name: "Self-Hosted Professional",
PricePerSeat: 10,
SKU: "professional",
RecurringInterval: model.RecurringIntervalYearly,
},
{
ID: "prod_test2",
Name: "Self-Hosted Enterprise",
PricePerSeat: 30,
SKU: "enterprise",
RecurringInterval: model.RecurringIntervalYearly,
},
}
t.Run("get products for admins", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSelfHostedProducts", mock.Anything, mock.Anything).Return(products, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
returnedProducts, r, err := th.Client.GetSelfHostedProducts()
require.NoError(t, err)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
require.Equal(t, returnedProducts, products)
})
t.Run("get products for non admins", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
cloud := mocks.CloudInterface{}
cloud.Mock.On("GetSelfHostedProducts", mock.Anything, mock.Anything).Return(products, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
returnedProducts, r, err := th.Client.GetSelfHostedProducts()
require.NoError(t, err)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
require.Equal(t, returnedProducts, sanitizedProducts)
// make a more explicit check
require.Equal(t, returnedProducts[0].ID, "prod_test")
require.Equal(t, returnedProducts[0].Name, "Self-Hosted Professional")
require.Equal(t, returnedProducts[0].SKU, "professional")
require.Equal(t, returnedProducts[0].PricePerSeat, float64(10))
require.Equal(t, returnedProducts[0].Description, "")
require.Equal(t, returnedProducts[0].PriceID, "")
require.Equal(t, returnedProducts[0].Family, model.SubscriptionFamily(""))
require.Equal(t, returnedProducts[0].RecurringInterval, model.RecurringInterval("year"))
require.Equal(t, returnedProducts[0].BillingScheme, model.BillingScheme(""))
require.Equal(t, returnedProducts[0].CrossSellsTo, "")
require.Equal(t, returnedProducts[1].ID, "prod_test2")
require.Equal(t, returnedProducts[1].Name, "Self-Hosted Enterprise")
require.Equal(t, returnedProducts[1].SKU, "enterprise")
require.Equal(t, returnedProducts[1].PricePerSeat, float64(30))
require.Equal(t, returnedProducts[1].Description, "")
require.Equal(t, returnedProducts[1].PriceID, "")
require.Equal(t, returnedProducts[1].Family, model.SubscriptionFamily(""))
require.Equal(t, returnedProducts[1].RecurringInterval, model.RecurringInterval("year"))
require.Equal(t, returnedProducts[1].BillingScheme, model.BillingScheme(""))
require.Equal(t, returnedProducts[1].CrossSellsTo, "")
})
}

35
server/channels/api4/cluster.go Обычный файл
Просмотреть файл

@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitCluster() {
api.BaseRoutes.Cluster.Handle("/status", api.APISessionRequired(getClusterStatus)).Methods("GET")
}
func getClusterStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadEnvironmentHighAvailability) {
c.SetPermissionError(model.PermissionSysconsoleReadEnvironmentHighAvailability)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("getClusterStatus", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
infos := c.App.GetClusterStatus()
js, err := json.Marshal(infos)
if err != nil {
c.Err = model.NewAppError("getClusterStatus", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}

38
server/channels/api4/cluster_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGetClusterStatus(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("as system user", func(t *testing.T) {
_, resp, err := th.Client.GetClusterStatus()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("as system admin", func(t *testing.T) {
infos, _, err := th.SystemAdminClient.GetClusterStatus()
require.NoError(t, err)
require.NotNil(t, infos, "cluster status should not be nil")
})
t.Run("as restricted system admin", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
_, resp, err := th.SystemAdminClient.GetClusterStatus()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}

488
server/channels/api4/command.go Обычный файл
Просмотреть файл

@@ -0,0 +1,488 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCommand() {
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(createCommand)).Methods("POST")
api.BaseRoutes.Commands.Handle("", api.APISessionRequired(listCommands)).Methods("GET")
api.BaseRoutes.Commands.Handle("/execute", api.APISessionRequired(executeCommand)).Methods("POST")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(getCommand)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(updateCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("/move", api.APISessionRequired(moveCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("", api.APISessionRequired(deleteCommand)).Methods("DELETE")
api.BaseRoutes.Team.Handle("/commands/autocomplete", api.APISessionRequired(listAutocompleteCommands)).Methods("GET")
api.BaseRoutes.Team.Handle("/commands/autocomplete_suggestions", api.APISessionRequired(listCommandAutocompleteSuggestions)).Methods("GET")
api.BaseRoutes.Command.Handle("/regen_token", api.APISessionRequired(regenCommandToken)).Methods("PUT")
}
func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
cmd.CreatorId = c.AppContext.Session().UserId
rcmd, err := c.App.CreateCommand(&cmd)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil || cmd.Id != c.Params.CommandId {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("updateCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
oldCmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(oldCmd)
if cmd.TeamId != oldCmd.TeamId {
c.Err = model.NewAppError("updateCommand", "api.command.team_mismatch.app_error", nil, "user_id="+c.AppContext.Session().UserId, http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != oldCmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), oldCmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
rcmd, err := c.App.UpdateCommand(oldCmd, &cmd)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func moveCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
var cmr model.CommandMoveRequest
if jsonErr := json.NewDecoder(r.Body).Decode(&cmr); jsonErr != nil {
c.SetInvalidParamWithErr("team_id", jsonErr)
return
}
auditRec := c.MakeAuditRecord("moveCommand", audit.Fail)
audit.AddEventParameter(auditRec, "command_move_request", cmr.TeamId)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
newTeam, appErr := c.App.GetTeam(cmr.TeamId)
if appErr != nil {
c.Err = appErr
return
}
audit.AddEventParameterAuditable(auditRec, "team", newTeam)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), newTeam.Id, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
cmd, appErr := c.App.GetCommand(c.Params.CommandId)
if appErr != nil {
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if appErr = c.App.MoveCommand(newTeam, cmd); appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(cmd)
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteCommand", audit.Fail)
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
err = c.App.DeleteCommand(cmd.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventObjectType("command")
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func listCommands(c *Context, w http.ResponseWriter, r *http.Request) {
customOnly, _ := strconv.ParseBool(r.URL.Query().Get("custom_only"))
teamId := r.URL.Query().Get("team_id")
if teamId == "" {
c.SetInvalidParam("team_id")
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
var commands []*model.Command
var err *model.AppError
if customOnly {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
c.SetPermissionError(model.PermissionManageSlashCommands)
return
}
commands, err = c.App.ListTeamCommands(teamId)
if err != nil {
c.Err = err
return
}
} else {
//User with no permission should see only system commands
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamId, model.PermissionManageSlashCommands) {
commands, err = c.App.ListAutocompleteCommands(teamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
} else {
commands, err = c.App.ListAllCommands(teamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
}
}
if err := json.NewEncoder(w).Encode(commands); err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getCommand(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
c.SetCommandNotFoundError()
return
}
// check for permissions to view this command; must have perms to view team and
// PERMISSION_MANAGE_SLASH_COMMANDS for the team the command belongs to.
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionViewTeam) {
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
// again, return not_found to ensure id existence does not leak.
c.SetCommandNotFoundError()
return
}
if err := json.NewEncoder(w).Encode(cmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var commandArgs model.CommandArgs
if jsonErr := json.NewDecoder(r.Body).Decode(&commandArgs); jsonErr != nil {
c.SetInvalidParamWithErr("command_args", jsonErr)
return
}
if len(commandArgs.Command) <= 1 || strings.Index(commandArgs.Command, "/") != 0 || !model.IsValidId(commandArgs.ChannelId) {
c.Err = model.NewAppError("executeCommand", "api.command.execute_command.start.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("executeCommand", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
// checks that user is a member of the specified channel, and that they have permission to use slash commands in it
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
c.SetPermissionError(model.PermissionUseSlashCommands)
return
}
channel, err := c.App.GetChannel(c.AppContext, commandArgs.ChannelId)
if err != nil {
c.Err = err
return
}
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup {
// if this isn't a DM or GM, the team id is implicitly taken from the channel so that slash commands created on
// some other team can't be run against this one
commandArgs.TeamId = channel.TeamId
} else {
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
// they can't just execute slash commands against arbitrary teams
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
c.SetPermissionError(model.PermissionUseSlashCommands)
return
}
}
}
commandArgs.UserId = c.AppContext.Session().UserId
commandArgs.T = c.AppContext.T
commandArgs.SiteURL = c.GetSiteURLHeader()
commandArgs.Session = *c.AppContext.Session()
response, err := c.App.ExecuteCommand(c.AppContext, &commandArgs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listAutocompleteCommands(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
commands, err := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(commands); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func listCommandAutocompleteSuggestions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
roleId := model.SystemUserRoleId
if c.IsSystemAdmin() {
roleId = model.SystemAdminRoleId
}
query := r.URL.Query()
userInput := query.Get("user_input")
if userInput == "" {
c.SetInvalidParam("userInput")
return
}
userInput = strings.TrimPrefix(userInput, "/")
commands, appErr := c.App.ListAutocompleteCommands(c.Params.TeamId, c.AppContext.T)
if appErr != nil {
c.Err = appErr
return
}
commandArgs := &model.CommandArgs{
ChannelId: query.Get("channel_id"),
TeamId: c.Params.TeamId,
RootId: query.Get("root_id"),
UserId: c.AppContext.Session().UserId,
T: c.AppContext.T,
Session: *c.AppContext.Session(),
SiteURL: c.GetSiteURLHeader(),
Command: userInput,
}
suggestions := c.App.GetSuggestions(c.AppContext, commandArgs, commands, roleId)
js, err := json.Marshal(suggestions)
if err != nil {
c.Err = model.NewAppError("listCommandAutocompleteSuggestions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireCommandId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("regenCommandToken", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
cmd, err := c.App.GetCommand(c.Params.CommandId)
if err != nil {
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
c.SetCommandNotFoundError()
return
}
auditRec.AddEventPriorState(cmd)
audit.AddEventParameter(auditRec, "command_id", c.Params.CommandId)
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
// here we return Not_found instead of a permissions error so we don't leak the existence of
// a command to someone without permissions for the team it belongs to.
c.SetCommandNotFoundError()
return
}
if c.AppContext.Session().UserId != cmd.CreatorId && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), cmd.TeamId, model.PermissionManageOthersSlashCommands) {
c.LogAudit("fail - inappropriate permissions")
c.SetPermissionError(model.PermissionManageOthersSlashCommands)
return
}
rcmd, err := c.App.RegenCommandToken(cmd)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(rcmd)
auditRec.Success()
c.LogAudit("success")
resp := make(map[string]string)
resp["token"] = rcmd.Token
w.Write([]byte(model.MapToJSON(resp)))
}

38
server/channels/api4/command_help_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestHelpCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
HelpLink := *th.App.Config().SupportSettings.HelpLink
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.SupportSettings.HelpLink = HelpLink })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.SupportSettings.HelpLink = "" })
rs1, _, err := client.ExecuteCommand(channel.Id, "/help ")
require.NoError(t, err)
assert.Contains(t, rs1.Text, model.SupportSettingsDefaultHelpLink, "failed to default help link")
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.SupportSettings.HelpLink = "https://docs.mattermost.com/guides/user.html"
})
rs2, _, err := client.ExecuteCommand(channel.Id, "/help ")
require.NoError(t, err)
assert.Contains(t, rs2.Text, "https://docs.mattermost.com/guides/user.html", "failed to help link")
}

52
server/channels/api4/command_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCommandLocal() {
api.BaseRoutes.Commands.Handle("", api.APILocal(localCreateCommand)).Methods("POST")
api.BaseRoutes.Commands.Handle("", api.APILocal(listCommands)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APILocal(getCommand)).Methods("GET")
api.BaseRoutes.Command.Handle("", api.APILocal(updateCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("/move", api.APILocal(moveCommand)).Methods("PUT")
api.BaseRoutes.Command.Handle("", api.APILocal(deleteCommand)).Methods("DELETE")
}
func localCreateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
var cmd model.Command
if jsonErr := json.NewDecoder(r.Body).Decode(&cmd); jsonErr != nil {
c.SetInvalidParamWithErr("command", jsonErr)
return
}
auditRec := c.MakeAuditRecord("localCreateCommand", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "command", &cmd)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
rcmd, err := c.App.CreateCommand(&cmd)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
auditRec.AddEventResultState(rcmd)
auditRec.AddEventObjectType("command")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rcmd); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}

1067
server/channels/api4/command_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

487
server/channels/api4/commands_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,487 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
_ "github.com/mattermost/mattermost-server/v6/server/channels/app/slashcommands"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestEchoCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel1 := th.BasicChannel
echoTestString := "/echo test"
r1, _, err := client.ExecuteCommand(channel1.Id, echoTestString)
require.NoError(t, err)
require.NotNil(t, r1, "Echo command failed to execute")
r1, _, err = client.ExecuteCommand(channel1.Id, "/echo ")
require.NoError(t, err)
require.NotNil(t, r1, "Echo command failed to execute")
time.Sleep(100 * time.Millisecond)
p1, _, err := client.GetPostsForChannel(channel1.Id, 0, 2, "", false, false)
require.NoError(t, err)
require.Len(t, p1.Order, 2, "Echo command failed to send")
}
func TestGroupmsgCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
team := th.BasicTeam
user1 := th.BasicUser
user2 := th.BasicUser2
user3 := th.CreateUser()
user4 := th.CreateUser()
user5 := th.CreateUser()
user6 := th.CreateUser()
user7 := th.CreateUser()
user8 := th.CreateUser()
user9 := th.CreateUser()
th.LinkUserToTeam(user3, team)
th.LinkUserToTeam(user4, team)
rs1, _, err := client.ExecuteCommand(th.BasicChannel.Id, "/groupmsg "+user2.Username+","+user3.Username)
require.NoError(t, err)
group1 := model.GetGroupNameFromUserIds([]string{user1.Id, user2.Id, user3.Id})
require.True(t, strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+group1), "failed to create group channel")
rs2, _, err := client.ExecuteCommand(th.BasicChannel.Id, "/groupmsg "+user3.Username+","+user4.Username+" foobar")
require.NoError(t, err)
group2 := model.GetGroupNameFromUserIds([]string{user1.Id, user3.Id, user4.Id})
require.True(t, strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+group2), "failed to create second direct channel")
result, _, err := client.SearchPosts(team.Id, "foobar", false)
require.NoError(t, err)
require.NotEqual(t, 0, len(result.Order), "post did not get sent to direct message")
rs3, _, err := client.ExecuteCommand(th.BasicChannel.Id, "/groupmsg "+user2.Username+","+user3.Username)
require.NoError(t, err)
require.True(t, strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+group1), "failed to go back to existing group channel")
_, _, err = client.ExecuteCommand(th.BasicChannel.Id, "/groupmsg "+user2.Username+" foobar")
require.NoError(t, err)
_, _, err = client.ExecuteCommand(th.BasicChannel.Id, "/groupmsg "+user2.Username+","+user3.Username+","+user4.Username+","+user5.Username+","+user6.Username+","+user7.Username+","+user8.Username+","+user9.Username+" foobar")
require.NoError(t, err)
_, _, err = client.ExecuteCommand(th.BasicChannel.Id, "/groupmsg junk foobar")
require.NoError(t, err)
_, _, err = client.ExecuteCommand(th.BasicChannel.Id, "/groupmsg junk,junk2 foobar")
require.NoError(t, err)
}
func TestInvitePeopleCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
r1, _, err := client.ExecuteCommand(channel.Id, "/invite_people test@example.com")
require.NoError(t, err)
require.NotNil(t, r1, "Command failed to execute")
r2, _, err := client.ExecuteCommand(channel.Id, "/invite_people test1@example.com test2@example.com")
require.NoError(t, err)
require.NotNil(t, r2, "Command failed to execute")
r3, _, err := client.ExecuteCommand(channel.Id, "/invite_people")
require.NoError(t, err)
require.NotNil(t, r3, "Command failed to execute")
}
// also used to test /open (see command_open_test.go)
func testJoinCommands(t *testing.T, alias string) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
team := th.BasicTeam
user2 := th.BasicUser2
channel0 := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
channel0, _, err := client.CreateChannel(channel0)
require.NoError(t, err)
channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
channel1, _, err = client.CreateChannel(channel1)
require.NoError(t, err)
_, err = client.RemoveUserFromChannel(channel1.Id, th.BasicUser.Id)
require.NoError(t, err)
channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
channel2, _, err = client.CreateChannel(channel2)
require.NoError(t, err)
_, err = client.RemoveUserFromChannel(channel2.Id, th.BasicUser.Id)
require.NoError(t, err)
channel3, _, err := client.CreateDirectChannel(th.BasicUser.Id, user2.Id)
require.NoError(t, err)
rs5, _, err := client.ExecuteCommand(channel0.Id, "/"+alias+" "+channel2.Name)
require.NoError(t, err)
require.True(t, strings.HasSuffix(rs5.GotoLocation, "/"+team.Name+"/channels/"+channel2.Name), "failed to join channel")
rs6, _, err := client.ExecuteCommand(channel0.Id, "/"+alias+" "+channel3.Name)
require.NoError(t, err)
require.False(t, strings.HasSuffix(rs6.GotoLocation, "/"+team.Name+"/channels/"+channel3.Name), "should not have joined direct message channel")
c1, _, err := client.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, false, "")
require.NoError(t, err)
found := false
for _, c := range c1 {
if c.Id == channel2.Id {
found = true
}
}
require.True(t, found, "did not join channel")
// test case insensitively
channel4 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
channel4, _, err = client.CreateChannel(channel4)
require.NoError(t, err)
_, err = client.RemoveUserFromChannel(channel4.Id, th.BasicUser.Id)
require.NoError(t, err)
rs7, _, err := client.ExecuteCommand(channel0.Id, "/"+alias+" "+strings.ToUpper(channel4.Name))
require.NoError(t, err)
require.True(t, strings.HasSuffix(rs7.GotoLocation, "/"+team.Name+"/channels/"+channel4.Name), "failed to join channel")
}
func TestJoinCommands(t *testing.T) {
testJoinCommands(t, "join")
}
func TestLoadTestHelpCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
rs, _, err := client.ExecuteCommand(channel.Id, "/test help")
require.NoError(t, err)
require.True(t, strings.Contains(rs.Text, "Mattermost testing commands to help"), rs.Text)
time.Sleep(2 * time.Second)
}
func TestLoadTestSetupCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
rs, _, err := client.ExecuteCommand(channel.Id, "/test setup fuzz 1 1 1")
require.NoError(t, err)
require.Equal(t, "Created environment", rs.Text, rs.Text)
time.Sleep(2 * time.Second)
}
func TestLoadTestUsersCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
rs, _, err := client.ExecuteCommand(channel.Id, "/test users fuzz 1 2")
require.NoError(t, err)
require.Equal(t, "Added users", rs.Text, rs.Text)
time.Sleep(2 * time.Second)
}
func TestLoadTestChannelsCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
rs, _, err := client.ExecuteCommand(channel.Id, "/test channels fuzz 1 2")
require.NoError(t, err)
require.Equal(t, "Added channels", rs.Text, rs.Text)
time.Sleep(2 * time.Second)
}
func TestLoadTestPostsCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
enableTesting := *th.App.Config().ServiceSettings.EnableTesting
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = enableTesting })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableTesting = true })
rs, _, err := client.ExecuteCommand(channel.Id, "/test posts fuzz 2 3 2")
require.NoError(t, err)
require.Equal(t, "Added posts", rs.Text, rs.Text)
time.Sleep(2 * time.Second)
}
func TestLeaveCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
team := th.BasicTeam
user2 := th.BasicUser2
channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: team.Id}
channel1, _, err := client.CreateChannel(channel1)
require.NoError(t, err)
_, _, err = client.AddChannelMember(channel1.Id, th.BasicUser.Id)
require.NoError(t, err)
channel2 := &model.Channel{DisplayName: "BB", Name: "bb" + model.NewId() + "a", Type: model.ChannelTypePrivate, TeamId: team.Id}
channel2, _, err = client.CreateChannel(channel2)
require.NoError(t, err)
_, _, err = client.AddChannelMember(channel2.Id, th.BasicUser.Id)
require.NoError(t, err)
_, _, err = client.AddChannelMember(channel2.Id, user2.Id)
require.NoError(t, err)
channel3, _, err := client.CreateDirectChannel(th.BasicUser.Id, user2.Id)
require.NoError(t, err)
rs1, _, err := client.ExecuteCommand(channel1.Id, "/leave")
require.NoError(t, err)
require.True(t, strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+model.DefaultChannelName), "failed to leave open channel 1")
rs2, _, err := client.ExecuteCommand(channel2.Id, "/leave")
require.NoError(t, err)
require.True(t, strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+model.DefaultChannelName), "failed to leave private channel 1")
_, _, err = client.ExecuteCommand(channel3.Id, "/leave")
require.Error(t, err)
cdata, _, err := client.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, false, "")
require.NoError(t, err)
found := false
for _, c := range cdata {
if c.Id == channel1.Id || c.Id == channel2.Id {
found = true
}
}
require.False(t, found, "did not leave right channels")
for _, c := range cdata {
if c.Name == model.DefaultChannelName {
_, err := client.RemoveUserFromChannel(c.Id, th.BasicUser.Id)
require.Error(t, err, "should have errored on leaving default channel")
break
}
}
}
func TestLogoutTestCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
_, _, err := th.Client.ExecuteCommand(th.BasicChannel.Id, "/logout")
require.NoError(t, err)
}
func TestMeCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
testString := "/me hello"
r1, _, err := client.ExecuteCommand(channel.Id, testString)
require.NoError(t, err)
require.NotNil(t, r1, "Command failed to execute")
time.Sleep(100 * time.Millisecond)
p1, _, err := client.GetPostsForChannel(channel.Id, 0, 2, "", false, false)
require.NoError(t, err)
require.Len(t, p1.Order, 2, "Command failed to send")
pt := p1.Posts[p1.Order[0]].Type
require.Equal(t, model.PostTypeMe, pt, "invalid post type")
msg := p1.Posts[p1.Order[0]].Message
want := "*hello*"
require.Equal(t, want, msg, "invalid me response")
}
func TestMsgCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
team := th.BasicTeam
user1 := th.BasicUser
user2 := th.BasicUser2
user3 := th.CreateUser()
th.LinkUserToTeam(user3, team)
_, _, err := client.CreateDirectChannel(th.BasicUser.Id, user2.Id)
require.NoError(t, err)
_, _, err = client.CreateDirectChannel(th.BasicUser.Id, user3.Id)
require.NoError(t, err)
rs1, _, err := client.ExecuteCommand(th.BasicChannel.Id, "/msg "+user2.Username)
require.NoError(t, err)
require.Condition(t, func() bool {
return strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user2.Id) ||
strings.HasSuffix(rs1.GotoLocation, "/"+team.Name+"/channels/"+user2.Id+"__"+user1.Id)
}, "failed to create direct channel")
rs2, _, err := client.ExecuteCommand(th.BasicChannel.Id, "/msg "+user3.Username+" foobar")
require.NoError(t, err)
require.Condition(t, func() bool {
return strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user3.Id) ||
strings.HasSuffix(rs2.GotoLocation, "/"+team.Name+"/channels/"+user3.Id+"__"+user1.Id)
}, "failed to create second direct channel")
result, _, err := client.SearchPosts(th.BasicTeam.Id, "foobar", false)
require.NoError(t, err)
require.NotEqual(t, 0, len(result.Order), "post did not get sent to direct message")
rs3, _, err := client.ExecuteCommand(th.BasicChannel.Id, "/msg "+user2.Username)
require.NoError(t, err)
require.Condition(t, func() bool {
return strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+user1.Id+"__"+user2.Id) ||
strings.HasSuffix(rs3.GotoLocation, "/"+team.Name+"/channels/"+user2.Id+"__"+user1.Id)
}, "failed to go back to existing direct channel")
_, _, err = client.ExecuteCommand(th.BasicChannel.Id, "/msg "+th.BasicUser.Username+" foobar")
require.NoError(t, err)
_, _, err = client.ExecuteCommand(th.BasicChannel.Id, "/msg junk foobar")
require.NoError(t, err)
}
func TestOpenCommands(t *testing.T) {
testJoinCommands(t, "open")
}
func TestSearchCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
_, _, err := th.Client.ExecuteCommand(th.BasicChannel.Id, "/search")
require.NoError(t, err)
}
func TestSettingsCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
_, _, err := th.Client.ExecuteCommand(th.BasicChannel.Id, "/settings")
require.NoError(t, err)
}
func TestShortcutsCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
_, _, err := th.Client.ExecuteCommand(th.BasicChannel.Id, "/shortcuts")
require.NoError(t, err)
}
func TestShrugCommand(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
channel := th.BasicChannel
testString := "/shrug"
r1, _, err := client.ExecuteCommand(channel.Id, testString)
require.NoError(t, err)
require.NotNil(t, r1, "Command failed to execute")
time.Sleep(100 * time.Millisecond)
p1, _, err := client.GetPostsForChannel(channel.Id, 0, 2, "", false, false)
require.NoError(t, err)
require.Len(t, p1.Order, 2, "Command failed to send")
require.Equal(t, `¯\\\_(ツ)\_/¯`, p1.Posts[p1.Order[0]].Message, "invalid shrug response")
}
func TestStatusCommands(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
commandAndTest(t, th, "away")
commandAndTest(t, th, "offline")
commandAndTest(t, th, "online")
}
func commandAndTest(t *testing.T, th *TestHelper, status string) {
client := th.Client
channel := th.BasicChannel
user := th.BasicUser
r1, _, err := client.ExecuteCommand(channel.Id, "/"+status)
require.NoError(t, err)
require.NotEqual(t, "Command failed to execute", r1)
time.Sleep(1000 * time.Millisecond)
rstatus, _, err := client.GetUserStatus(user.Id, "")
require.NoError(t, err)
require.Equal(t, status, rstatus.Status, "Error setting status")
}

162
server/channels/api4/compliance.go Обычный файл
Просмотреть файл

@@ -0,0 +1,162 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strconv"
"github.com/avct/uasurfer"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitCompliance() {
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(createComplianceReport)).Methods("POST")
api.BaseRoutes.Compliance.Handle("/reports", api.APISessionRequired(getComplianceReports)).Methods("GET")
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}", api.APISessionRequired(getComplianceReport)).Methods("GET")
api.BaseRoutes.Compliance.Handle("/reports/{report_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadComplianceReport)).Methods("GET")
}
func createComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
var job model.Compliance
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
c.SetInvalidParamWithErr("compliance", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createComplianceReport", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "compliance", &job)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateComplianceExportJob) {
c.SetPermissionError(model.PermissionCreateComplianceExportJob)
return
}
job.UserId = c.AppContext.Session().UserId
rjob, err := c.App.SaveComplianceReport(&job)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rjob)
auditRec.AddEventObjectType("compliance")
auditRec.AddMeta("compliance_id", rjob.Id)
auditRec.AddMeta("compliance_desc", rjob.Desc)
c.LogAudit("")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rjob); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getComplianceReports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
c.SetPermissionError(model.PermissionReadComplianceExportJob)
return
}
auditRec := c.MakeAuditRecord("getComplianceReports", audit.Fail)
defer c.LogAuditRec(auditRec)
crs, err := c.App.GetComplianceReports(c.Params.Page, c.Params.PerPage)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(crs); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireReportId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("getComplianceReport", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadComplianceExportJob) {
c.SetPermissionError(model.PermissionReadComplianceExportJob)
return
}
audit.AddEventParameter(auditRec, "report_id", c.Params.ReportId)
job, err := c.App.GetComplianceReport(c.Params.ReportId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddMeta("compliance_id", job.Id)
auditRec.AddMeta("compliance_desc", job.Desc)
if err := json.NewEncoder(w).Encode(job); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireReportId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("downloadComplianceReport", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "compliance_id", c.Params.ReportId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
return
}
job, err := c.App.GetComplianceReport(c.Params.ReportId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(job)
auditRec.AddEventObjectType("compliance")
reportBytes, err := c.App.GetComplianceFile(job)
if err != nil {
c.Err = err
return
}
auditRec.AddMeta("length", len(reportBytes))
c.LogAudit("downloaded " + job.Desc)
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Length", strconv.Itoa(len(reportBytes)))
w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
// attach extra headers to trigger a download on IE, Edge, and Safari
ua := uasurfer.Parse(r.UserAgent())
w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
if ua.Browser.Name == uasurfer.BrowserIE || ua.Browser.Name == uasurfer.BrowserSafari {
// trim off anything before the final / so we just get the file's name
w.Header().Set("Content-Type", "application/octet-stream")
}
auditRec.Success()
w.Write(reportBytes)
}

443
server/channels/api4/config.go Обычный файл
Просмотреть файл

@@ -0,0 +1,443 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var writeFilter func(c *Context, structField reflect.StructField) bool
var readFilter func(c *Context, structField reflect.StructField) bool
var permissionMap map[string]*model.Permission
type filterType string
const (
FilterTypeWrite filterType = "write"
FilterTypeRead filterType = "read"
)
func (api *API) InitConfig() {
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(getConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config", api.APISessionRequired(updateConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APISessionRequired(patchConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APISessionRequired(configReload)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/config/client", api.APIHandler(getClientConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config/environment", api.APISessionRequired(getEnvironmentConfig)).Methods("GET")
}
func init() {
writeFilter = makeFilterConfigByPermission(FilterTypeWrite)
readFilter = makeFilterConfigByPermission(FilterTypeRead)
permissionMap = map[string]*model.Permission{}
for _, p := range model.AllPermissions {
permissionMap[p.Id] = p
}
}
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
c.SetPermissionError(model.SysconsoleReadPermissions...)
return
}
auditRec := c.MakeAuditRecord("getConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg, err := config.Merge(&model.Config{}, c.App.GetSanitizedConfig(), &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("getConfig", "api.config.get_config.restricted_merge.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, jsonErr := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if jsonErr != nil {
c.Err = model.NewAppError("getConfig", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func configReload(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("configReload", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReloadConfig) {
c.SetPermissionError(model.PermissionReloadConfig)
return
}
if !c.AppContext.Session().IsUnrestricted() && *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("configReload", "api.restricted_system_admin", nil, "", http.StatusBadRequest)
return
}
if err := c.App.ReloadConfig(); err != nil {
c.Err = model.NewAppError("configReload", "api.config.reload_config.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
ReturnStatusOK(w)
}
func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("updateConfig", audit.Fail)
// audit.AddEventParameter(auditRec, "config", cfg) // TODO We can do this but do we want to?
defer c.LogAuditRec(auditRec)
cfg.SetDefaults()
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
c.SetPermissionError(model.SysconsoleWritePermissions...)
return
}
appCfg := c.App.Config()
if *appCfg.ServiceSettings.SiteURL != "" && *cfg.ServiceSettings.SiteURL == "" {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
return
}
cfg, err = config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return writeFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
// Do not allow plugin uploads to be toggled through the API
*cfg.PluginSettings.EnableUploads = *appCfg.PluginSettings.EnableUploads
// Do not allow certificates to be changed through the API
// This shallow-copies the slice header. So be careful if there are concurrent
// modifications to the slice.
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
// Do not allow marketplace URL to be toggled through the API if EnableUploads are disabled.
if cfg.PluginSettings.EnableUploads != nil && !*appCfg.PluginSettings.EnableUploads {
*cfg.PluginSettings.MarketplaceURL = *appCfg.PluginSettings.MarketplaceURL
}
if cfg.PluginSettings.PluginStates[model.PluginIdFocalboard].Enable && cfg.FeatureFlags.BoardsProduct {
c.Err = model.NewAppError("EnablePlugin", "app.plugin.product_mode.app_error", map[string]any{"Name": model.PluginIdFocalboard}, "", http.StatusInternalServerError)
return
}
// There are some settings that cannot be changed in a cloud env
if c.App.Channels().License().IsCloud() {
// Both of them cannot be nil since cfg.SetDefaults is called earlier for cfg,
// and appCfg is the existing earlier config and if it's nil, server sets a default value.
if *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
return
}
}
c.App.HandleMessageExportConfig(cfg, appCfg)
if appErr := cfg.IsValid(); appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
if appErr != nil {
c.Err = appErr
return
}
// If the config for default server locale has changed, reinitialize the server's translations.
if oldCfg.LocalizationSettings.DefaultServerLocale != newCfg.LocalizationSettings.DefaultServerLocale {
s := newCfg.LocalizationSettings
if err = i18n.InitTranslations(*s.DefaultServerLocale, *s.DefaultClientLocale); err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.translations.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
//auditRec.AddEventResultState(cfg) // TODO we can do this too but do we want to? the config object is huge
auditRec.AddEventObjectType("config")
auditRec.Success()
c.LogAudit("updateConfig")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if err != nil {
c.Err = model.NewAppError("updateConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
if format == "" {
c.Err = model.NewAppError("getClientConfig", "api.config.client.old_format.app_error", nil, "", http.StatusNotImplemented)
return
}
if format != "old" {
c.SetInvalidParam("format")
return
}
var config map[string]string
if c.AppContext.Session().UserId == "" {
config = c.App.Srv().Platform().LimitedClientConfigWithComputed()
} else {
config = c.App.Srv().Platform().ClientConfigWithComputed()
}
w.Write([]byte(model.MapToJSON(config)))
}
func getEnvironmentConfig(c *Context, w http.ResponseWriter, r *http.Request) {
// Only return the environment variables for the subsections which the client is
// allowed to see
envConfig := c.App.GetEnvironmentConfig(func(structField reflect.StructField) bool {
return readFilter(c, structField)
})
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Write([]byte(model.StringInterfaceToJSON(envConfig)))
}
func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("patchConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleWritePermissions) {
c.SetPermissionError(model.SysconsoleWritePermissions...)
return
}
appCfg := c.App.Config()
if *appCfg.ServiceSettings.SiteURL != "" && cfg.ServiceSettings.SiteURL != nil && *cfg.ServiceSettings.SiteURL == "" {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.clear_siteurl.app_error", nil, "", http.StatusBadRequest)
return
}
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
return writeFilter(c, structField)
}
// Do not allow plugin uploads to be toggled through the API
if cfg.PluginSettings.EnableUploads != nil && *cfg.PluginSettings.EnableUploads != *appCfg.PluginSettings.EnableUploads {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.EnableUploads"}, "", http.StatusForbidden)
return
}
// Do not allow marketplace URL to be toggled if plugin uploads are disabled.
if cfg.PluginSettings.MarketplaceURL != nil && cfg.PluginSettings.EnableUploads != nil {
// Breaking it down to 2 conditions to make it simple.
if *cfg.PluginSettings.MarketplaceURL != *appCfg.PluginSettings.MarketplaceURL && !*cfg.PluginSettings.EnableUploads {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "PluginSettings.MarketplaceURL"}, "", http.StatusForbidden)
return
}
}
// There are some settings that cannot be changed in a cloud env
if c.App.Channels().License().IsCloud() {
if cfg.ComplianceSettings.Directory != nil && *appCfg.ComplianceSettings.Directory != *cfg.ComplianceSettings.Directory {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ComplianceSettings.Directory"}, "", http.StatusForbidden)
return
}
}
if cfg.MessageExportSettings.EnableExport != nil {
c.App.HandleMessageExportConfig(cfg, appCfg)
}
updatedCfg, err := config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: filterFn,
})
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
appErr := updatedCfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(updatedCfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
auditRec.Success()
cfg, err = config.Merge(&model.Config{}, newCfg, &utils.MergeConfig{
StructFieldFilter: func(structField reflect.StructField, base, patch reflect.Value) bool {
return readFilter(c, structField)
},
})
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.restricted_merge.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if c.App.Channels().License().IsCloud() {
js, err := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
return
}
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func makeFilterConfigByPermission(accessType filterType) func(c *Context, structField reflect.StructField) bool {
return func(c *Context, structField reflect.StructField) bool {
if structField.Type.Kind() == reflect.Struct {
return true
}
tagPermissions := strings.Split(structField.Tag.Get("access"), ",")
// If there are no access tag values and the role has manage_system, no need to continue
// checking permissions.
if len(tagPermissions) == 0 {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
return true
}
}
// one iteration for write_restrictable value, it could be anywhere in the order of values
for _, val := range tagPermissions {
tagValue := strings.TrimSpace(val)
if tagValue == "" {
continue
}
// ConfigAccessTagWriteRestrictable trumps all other permissions
if tagValue == model.ConfigAccessTagWriteRestrictable || tagValue == model.ConfigAccessTagCloudRestrictable {
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin && accessType == FilterTypeWrite {
return false
}
continue
}
}
// another iteration for permissions checks of other tag values
for _, val := range tagPermissions {
tagValue := strings.TrimSpace(val)
if tagValue == "" {
continue
}
if tagValue == model.ConfigAccessTagWriteRestrictable {
continue
}
if tagValue == model.ConfigAccessTagCloudRestrictable {
continue
}
if tagValue == model.ConfigAccessTagAnySysConsoleRead && accessType == FilterTypeRead &&
c.App.SessionHasPermissionToAny(*c.AppContext.Session(), model.SysconsoleReadPermissions) {
return true
}
permissionID := fmt.Sprintf("sysconsole_%s_%s", accessType, tagValue)
if permission, ok := permissionMap[permissionID]; ok {
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), permission) {
return true
}
} else {
mlog.Warn("Unrecognized config permissions tag value.", mlog.String("tag_value", permissionID))
}
}
// with manage_system, default to allow, otherwise default not-allow
return c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
}
}

175
server/channels/api4/config_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,175 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"reflect"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitConfigLocal() {
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localGetConfig)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/config", api.APILocal(localUpdateConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/patch", api.APILocal(localPatchConfig)).Methods("PUT")
api.BaseRoutes.APIRoot.Handle("/config/reload", api.APILocal(configReload)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/config/migrate", api.APILocal(localMigrateConfig)).Methods("POST")
}
func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localGetConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg := c.App.GetSanitizedConfig()
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(cfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localUpdateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("localUpdateConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
cfg.SetDefaults()
appCfg := c.App.Config()
// Do not allow plugin uploads to be toggled through the API
cfg.PluginSettings.EnableUploads = appCfg.PluginSettings.EnableUploads
// Do not allow certificates to be changed through the API
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
c.App.HandleMessageExportConfig(cfg, appCfg)
appErr := cfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(cfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, diffErr := config.Diff(oldCfg, newCfg)
if diffErr != nil {
c.Err = model.NewAppError("updateConfig", "api.config.update_config.diff.app_error", nil, diffErr.Error(), http.StatusInternalServerError)
return
}
auditRec.AddEventPriorState(&diffs)
newCfg.Sanitize()
auditRec.Success()
c.LogAudit("updateConfig")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(newCfg); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localPatchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil || cfg == nil {
c.SetInvalidParamWithErr("config", err)
return
}
auditRec := c.MakeAuditRecord("localPatchConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
appCfg := c.App.Config()
filterFn := func(structField reflect.StructField, base, patch reflect.Value) bool {
return true
}
if cfg.MessageExportSettings.EnableExport != nil {
c.App.HandleMessageExportConfig(cfg, appCfg)
}
updatedCfg, mergeErr := config.Merge(appCfg, cfg, &utils.MergeConfig{
StructFieldFilter: filterFn,
})
if mergeErr != nil {
c.Err = model.NewAppError("patchConfig", "api.config.update_config.restricted_merge.app_error", nil, mergeErr.Error(), http.StatusInternalServerError)
return
}
appErr := updatedCfg.IsValid()
if appErr != nil {
c.Err = appErr
return
}
oldCfg, newCfg, appErr := c.App.SaveConfig(updatedCfg, true)
if appErr != nil {
c.Err = appErr
return
}
diffs, err := config.Diff(oldCfg, newCfg)
if err != nil {
c.Err = model.NewAppError("patchConfig", "api.config.patch_config.diff.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.AddEventPriorState(&diffs)
auditRec.Success()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(c.App.GetSanitizedConfig()); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localMigrateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
from, ok := props["from"].(string)
if !ok {
c.SetInvalidParam("from")
return
}
to, ok := props["to"].(string)
if !ok {
c.SetInvalidParam("to")
return
}
auditRec := c.MakeAuditRecord("migrateConfig", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
err := config.Migrate(from, to)
if err != nil {
c.Err = model.NewAppError("migrateConfig", "api.config.migrate_config.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
auditRec.Success()
ReturnStatusOK(w)
}

861
server/channels/api4/config_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,861 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/config"
)
func TestGetConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
_, resp, err := client.GetConfig()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
cfg, _, err := client.GetConfig()
require.NoError(t, err)
require.NotEqual(t, "", cfg.TeamSettings.SiteName)
if *cfg.LdapSettings.BindPassword != model.FakeSetting && *cfg.LdapSettings.BindPassword != "" {
require.FailNow(t, "did not sanitize properly")
}
require.Equal(t, model.FakeSetting, *cfg.FileSettings.PublicLinkSalt, "did not sanitize properly")
if *cfg.FileSettings.AmazonS3SecretAccessKey != model.FakeSetting && *cfg.FileSettings.AmazonS3SecretAccessKey != "" {
require.FailNow(t, "did not sanitize properly")
}
if *cfg.EmailSettings.SMTPPassword != model.FakeSetting && *cfg.EmailSettings.SMTPPassword != "" {
require.FailNow(t, "did not sanitize properly")
}
if *cfg.GitLabSettings.Secret != model.FakeSetting && *cfg.GitLabSettings.Secret != "" {
require.FailNow(t, "did not sanitize properly")
}
require.Equal(t, model.FakeSetting, *cfg.SqlSettings.DataSource, "did not sanitize properly")
require.Equal(t, model.FakeSetting, *cfg.SqlSettings.AtRestEncryptKey, "did not sanitize properly")
if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceReplicas, " "), model.FakeSetting) && len(cfg.SqlSettings.DataSourceReplicas) != 0 {
require.FailNow(t, "did not sanitize properly")
}
if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceSearchReplicas, " "), model.FakeSetting) && len(cfg.SqlSettings.DataSourceSearchReplicas) != 0 {
require.FailNow(t, "did not sanitize properly")
}
})
}
func TestGetConfigWithAccessTag(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// set some values so that we know they're not blank
mockVaryByHeader := model.NewId()
mockSupportEmail := model.NewId() + "@mattermost.com"
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.RateLimitSettings.VaryByHeader = mockVaryByHeader
cfg.SupportSettings.SupportEmail = &mockSupportEmail
})
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
// add read sysconsole environment config
th.AddPermissionToRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
cfg, _, err := th.Client.GetConfig()
require.NoError(t, err)
t.Run("Cannot read value without permission", func(t *testing.T) {
assert.Nil(t, cfg.SupportSettings.SupportEmail)
})
t.Run("Can read value with permission", func(t *testing.T) {
assert.Equal(t, mockVaryByHeader, cfg.RateLimitSettings.VaryByHeader)
})
t.Run("Contains Feature Flags", func(t *testing.T) {
assert.NotNil(t, cfg.FeatureFlags)
})
}
func TestGetConfigAnyFlagsAccess(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
_, resp, _ := th.Client.GetConfig()
t.Run("Check permissions error with no sysconsole read permission", func(t *testing.T) {
CheckForbiddenStatus(t, resp)
})
// add read sysconsole environment config
th.AddPermissionToRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadEnvironmentRateLimiting.Id, model.SystemUserRoleId)
cfg, _, err := th.Client.GetConfig()
require.NoError(t, err)
t.Run("Can read value with permission", func(t *testing.T) {
assert.NotNil(t, cfg.FeatureFlags)
})
}
func TestReloadConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
t.Run("as system user", func(t *testing.T) {
resp, err := client.ReloadConfig()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.ReloadConfig()
require.NoError(t, err)
}, "as system admin and local mode")
t.Run("as restricted system admin", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
resp, err := client.ReloadConfig()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestUpdateConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
cfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
_, resp, err := client.UpdateConfig(cfg)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
SiteName := th.App.Config().TeamSettings.SiteName
*cfg.TeamSettings.SiteName = "MyFancyName"
cfg, _, err = client.UpdateConfig(cfg)
require.NoError(t, err)
require.Equal(t, "MyFancyName", *cfg.TeamSettings.SiteName, "It should update the SiteName")
//Revert the change
cfg.TeamSettings.SiteName = SiteName
cfg, _, err = client.UpdateConfig(cfg)
require.NoError(t, err)
require.Equal(t, SiteName, cfg.TeamSettings.SiteName, "It should update the SiteName")
t.Run("Should set defaults for missing fields", func(t *testing.T) {
_, err = th.SystemAdminClient.DoAPIPut("/config", "{}")
require.NoError(t, err)
})
t.Run("Should fail with validation error if invalid config setting is passed", func(t *testing.T) {
//Revert the change
badcfg := cfg.Clone()
badcfg.PasswordSettings.MinimumLength = model.NewInt(4)
badcfg.PasswordSettings.MinimumLength = model.NewInt(4)
_, resp, err = client.UpdateConfig(badcfg)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "model.config.is_valid.password_length.app_error")
})
t.Run("Should not be able to modify PluginSettings.EnableUploads", func(t *testing.T) {
oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads
*cfg.PluginSettings.EnableUploads = !oldEnableUploads
cfg, _, err = client.UpdateConfig(cfg)
require.NoError(t, err)
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
cfg.PluginSettings.EnableUploads = nil
cfg, _, err = client.UpdateConfig(cfg)
require.NoError(t, err)
assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
})
t.Run("Should not be able to modify PluginSettings.SignaturePublicKeyFiles", func(t *testing.T) {
oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles
cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature")
cfg, _, err = client.UpdateConfig(cfg)
require.NoError(t, err)
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
cfg.PluginSettings.SignaturePublicKeyFiles = nil
cfg, _, err = client.UpdateConfig(cfg)
require.NoError(t, err)
assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
})
})
t.Run("Should not be able to modify PluginSettings.MarketplaceURL if EnableUploads is disabled", func(t *testing.T) {
oldURL := "hello.com"
newURL := "new.com"
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = false
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg2 := th.App.Config().Clone()
*cfg2.PluginSettings.MarketplaceURL = newURL
cfg2, _, err = th.SystemAdminClient.UpdateConfig(cfg2)
require.NoError(t, err)
assert.Equal(t, oldURL, *cfg2.PluginSettings.MarketplaceURL)
// Allowing uploads
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg2 = th.App.Config().Clone()
*cfg2.PluginSettings.MarketplaceURL = newURL
cfg2, _, err = th.SystemAdminClient.UpdateConfig(cfg2)
require.NoError(t, err)
assert.Equal(t, newURL, *cfg2.PluginSettings.MarketplaceURL)
})
t.Run("Should not be able to modify ComplianceSettings.Directory in cloud", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
defer th.App.Srv().RemoveLicense()
cfg2 := th.App.Config().Clone()
*cfg2.ComplianceSettings.Directory = "hellodir"
_, resp, err = th.SystemAdminClient.UpdateConfig(cfg2)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
siteURL := cfg.ServiceSettings.SiteURL
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
nonEmptyURL := "http://localhost"
cfg.ServiceSettings.SiteURL = &nonEmptyURL
// Set the SiteURL
cfg, _, err = th.SystemAdminClient.UpdateConfig(cfg)
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
// Check that the Site URL can't be cleared
cfg.ServiceSettings.SiteURL = sToP("")
cfg, resp, err = th.SystemAdminClient.UpdateConfig(cfg)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "api.config.update_config.clear_siteurl.app_error")
// Check that the Site URL wasn't cleared
cfg, _, err = th.SystemAdminClient.GetConfig()
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
})
}
func TestGetConfigWithoutManageSystemPermission(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
t.Run("any sysconsole read permission provides config read access", func(t *testing.T) {
// forbidden by default
_, resp, err := th.Client.GetConfig()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// add any sysconsole read permission
th.AddPermissionToRole(model.SysconsoleReadPermissions[0].Id, model.SystemUserRoleId)
_, _, err = th.Client.GetConfig()
// should be readable now
require.NoError(t, err)
})
}
func TestUpdateConfigWithoutManageSystemPermission(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.Client.Login(th.BasicUser.Username, th.BasicUser.Password)
// add read sysconsole integrations config
th.AddPermissionToRole(model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, model.SystemUserRoleId)
t.Run("sysconsole read permission does not provides config write access", func(t *testing.T) {
// should be readable because has a sysconsole read permission
cfg, _, err := th.Client.GetConfig()
require.NoError(t, err)
_, resp, err := th.Client.UpdateConfig(cfg)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("the wrong write permission does not grant access", func(t *testing.T) {
// should be readable because has a sysconsole read permission
cfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
originalValue := *cfg.ServiceSettings.AllowCorsFrom
// add the wrong write permission
th.AddPermissionToRole(model.PermissionSysconsoleWriteAboutEditionAndLicense.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteAboutEditionAndLicense.Id, model.SystemUserRoleId)
// try update a config value allowed by sysconsole WRITE integrations
mockVal := model.NewId()
cfg.ServiceSettings.AllowCorsFrom = &mockVal
_, _, err = th.Client.UpdateConfig(cfg)
require.NoError(t, err)
// ensure the config setting was not updated
cfg, _, err = th.SystemAdminClient.GetConfig()
require.NoError(t, err)
assert.Equal(t, *cfg.ServiceSettings.AllowCorsFrom, originalValue)
})
t.Run("config value is writeable by specific system console permission", func(t *testing.T) {
// should be readable because has a sysconsole read permission
cfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
th.AddPermissionToRole(model.PermissionSysconsoleWriteIntegrationsCors.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteIntegrationsCors.Id, model.SystemUserRoleId)
th.AddPermissionToRole(model.PermissionSysconsoleReadIntegrationsCors.Id, model.SystemUserRoleId)
defer th.RemovePermissionFromRole(model.PermissionSysconsoleReadIntegrationsCors.Id, model.SystemUserRoleId)
// try update a config value allowed by sysconsole WRITE integrations
mockVal := model.NewId()
cfg.ServiceSettings.AllowCorsFrom = &mockVal
_, _, err = th.Client.UpdateConfig(cfg)
require.NoError(t, err)
// ensure the config setting was updated
cfg, _, err = th.Client.GetConfig()
require.NoError(t, err)
assert.Equal(t, *cfg.ServiceSettings.AllowCorsFrom, mockVal)
})
}
func TestUpdateConfigMessageExportSpecialHandling(t *testing.T) {
th := Setup(t)
defer th.TearDown()
messageExportEnabled := *th.App.Config().MessageExportSettings.EnableExport
messageExportTimestamp := *th.App.Config().MessageExportSettings.ExportFromTimestamp
defer th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MessageExportSettings.EnableExport = messageExportEnabled
*cfg.MessageExportSettings.ExportFromTimestamp = messageExportTimestamp
})
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MessageExportSettings.EnableExport = false
*cfg.MessageExportSettings.ExportFromTimestamp = int64(0)
})
// Turn it on, timestamp should be updated.
cfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = true
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
require.NoError(t, err)
assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.NotEqual(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
// Turn it off, timestamp should be cleared.
cfg, _, err = th.SystemAdminClient.GetConfig()
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = false
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
require.NoError(t, err)
assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
// Set a value from the config file.
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MessageExportSettings.EnableExport = false
*cfg.MessageExportSettings.ExportFromTimestamp = int64(12345)
})
// Turn it on, timestamp should *not* be updated.
cfg, _, err = th.SystemAdminClient.GetConfig()
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = true
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
require.NoError(t, err)
assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.Equal(t, int64(12345), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
// Turn it off, timestamp should be cleared.
cfg, _, err = th.SystemAdminClient.GetConfig()
require.NoError(t, err)
*cfg.MessageExportSettings.EnableExport = false
_, _, err = th.SystemAdminClient.UpdateConfig(cfg)
require.NoError(t, err)
assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
}
func TestUpdateConfigRestrictSystemAdmin(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
t.Run("Restrict flag should be honored for sysadmin", func(t *testing.T) {
originalCfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
cfg := originalCfg.Clone()
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
returnedCfg, _, err := th.SystemAdminClient.UpdateConfig(cfg)
require.NoError(t, err)
require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
require.Equal(t, *originalCfg.ServiceSettings.SiteURL, *returnedCfg.ServiceSettings.SiteURL)
actualCfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
require.Equal(t, returnedCfg, actualCfg)
})
t.Run("Restrict flag should be ignored by local mode", func(t *testing.T) {
originalCfg, _, err := th.LocalClient.GetConfig()
require.NoError(t, err)
cfg := originalCfg.Clone()
*cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
*cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
returnedCfg, _, err := th.LocalClient.UpdateConfig(cfg)
require.NoError(t, err)
require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
require.Equal(t, "http://example.com", *returnedCfg.ServiceSettings.SiteURL)
})
}
func TestUpdateConfigDiffInAuditRecord(t *testing.T) {
logFile, err := os.CreateTemp("", "adv.log")
require.NoError(t, err)
defer os.Remove(logFile.Name())
os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED", "true")
os.Setenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME", logFile.Name())
defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILEENABLED")
defer os.Unsetenv("MM_EXPERIMENTALAUDITSETTINGS_FILENAME")
options := []app.Option{app.WithLicense(model.NewTestLicense("advanced_logging"))}
th := SetupWithServerOptions(t, options)
defer th.TearDown()
cfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
timeoutVal := *cfg.ServiceSettings.ReadTimeout
cfg.ServiceSettings.ReadTimeout = model.NewInt(timeoutVal + 1)
cfg, _, err = th.SystemAdminClient.UpdateConfig(cfg)
require.NoError(t, err)
defer th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.ReadTimeout = model.NewInt(timeoutVal)
})
require.Equal(t, timeoutVal+1, *cfg.ServiceSettings.ReadTimeout)
// Forcing a flush before attempting to read log's content.
err = th.Server.Audit.Flush()
require.NoError(t, err)
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.NotEmpty(t, data)
require.Contains(t, string(data),
fmt.Sprintf(`"config_diffs":[{"actual_val":%d,"base_val":%d,"path":"ServiceSettings.ReadTimeout"}]`,
timeoutVal+1, timeoutVal))
}
func TestGetEnvironmentConfig(t *testing.T) {
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://example.mattermost.com")
os.Setenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI")
th := Setup(t)
defer th.TearDown()
t.Run("as system admin", func(t *testing.T) {
SystemAdminClient := th.SystemAdminClient
envConfig, _, err := SystemAdminClient.GetEnvironmentConfig()
require.NoError(t, err)
serviceSettings, ok := envConfig["ServiceSettings"]
require.True(t, ok, "should've returned ServiceSettings")
serviceSettingsAsMap, ok := serviceSettings.(map[string]any)
require.True(t, ok, "should've returned ServiceSettings as a map")
siteURL, ok := serviceSettingsAsMap["SiteURL"]
require.True(t, ok, "should've returned ServiceSettings.SiteURL")
siteURLAsBool, ok := siteURL.(bool)
require.True(t, ok, "should've returned ServiceSettings.SiteURL as a boolean")
require.True(t, siteURLAsBool, "should've returned ServiceSettings.SiteURL as true")
enableCustomEmoji, ok := serviceSettingsAsMap["EnableCustomEmoji"]
require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji")
enableCustomEmojiAsBool, ok := enableCustomEmoji.(bool)
require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji as a boolean")
require.True(t, enableCustomEmojiAsBool, "should've returned ServiceSettings.EnableCustomEmoji as true")
_, ok = envConfig["TeamSettings"]
require.False(t, ok, "should not have returned TeamSettings")
})
t.Run("as team admin", func(t *testing.T) {
TeamAdminClient := th.CreateClient()
th.LoginTeamAdminWithClient(TeamAdminClient)
envConfig, _, err := TeamAdminClient.GetEnvironmentConfig()
require.NoError(t, err)
require.Empty(t, envConfig)
})
t.Run("as regular user", func(t *testing.T) {
client := th.Client
envConfig, _, err := client.GetEnvironmentConfig()
require.NoError(t, err)
require.Empty(t, envConfig)
})
t.Run("as not-regular user", func(t *testing.T) {
client := th.CreateClient()
_, resp, err := client.GetEnvironmentConfig()
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}
func TestGetOldClientConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
testKey := "supersecretkey"
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.GoogleDeveloperKey = testKey })
t.Run("with session", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.GoogleDeveloperKey = testKey
})
client := th.Client
config, _, err := client.GetOldClientConfig("")
require.NoError(t, err)
require.NotEmpty(t, config["Version"], "config not returned correctly")
require.Equal(t, testKey, config["GoogleDeveloperKey"])
})
t.Run("without session", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.GoogleDeveloperKey = testKey
})
client := th.CreateClient()
config, _, err := client.GetOldClientConfig("")
require.NoError(t, err)
require.NotEmpty(t, config["Version"], "config not returned correctly")
require.Empty(t, config["GoogleDeveloperKey"], "config should be missing developer key")
})
t.Run("missing format", func(t *testing.T) {
client := th.Client
resp, err := client.DoAPIGet("/config/client", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
t.Run("invalid format", func(t *testing.T) {
client := th.Client
resp, err := client.DoAPIGet("/config/client?format=junk", "")
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
func TestPatchConfig(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("config is missing", func(t *testing.T) {
_, response, err := th.Client.PatchConfig(nil)
require.Error(t, err)
CheckBadRequestStatus(t, response)
})
t.Run("user is not system admin", func(t *testing.T) {
_, response, err := th.Client.PatchConfig(&model.Config{})
require.Error(t, err)
CheckForbiddenStatus(t, response)
})
t.Run("should not update the restricted fields when restrict toggle is on for sysadmin", func(t *testing.T) {
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
config := model.Config{LogSettings: model.LogSettings{
ConsoleLevel: model.NewString("INFO"),
}}
updatedConfig, _, _ := th.SystemAdminClient.PatchConfig(&config)
assert.Equal(t, "DEBUG", *updatedConfig.LogSettings.ConsoleLevel)
})
t.Run("should not bypass the restrict toggle if local client", func(t *testing.T) {
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
config := model.Config{LogSettings: model.LogSettings{
ConsoleLevel: model.NewString("INFO"),
}}
oldConfig, _, _ := th.LocalClient.GetConfig()
updatedConfig, _, _ := th.LocalClient.PatchConfig(&config)
assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
// reset the config
_, _, err := th.LocalClient.UpdateConfig(oldConfig)
require.NoError(t, err)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
t.Run("check if config is valid", func(t *testing.T) {
config := model.Config{PasswordSettings: model.PasswordSettings{
MinimumLength: model.NewInt(4),
}}
_, response, err := client.PatchConfig(&config)
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
assert.Error(t, err)
CheckErrorID(t, err, "model.config.is_valid.password_length.app_error")
})
t.Run("should patch the config", func(t *testing.T) {
*th.App.Config().ExperimentalSettings.RestrictSystemAdmin = false
th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalDefaultChannels = []string{"some-channel"} })
oldConfig, _, err := client.GetConfig()
require.NoError(t, err)
assert.False(t, *oldConfig.PasswordSettings.Lowercase)
assert.NotEqual(t, 15, *oldConfig.PasswordSettings.MinimumLength)
assert.Equal(t, "DEBUG", *oldConfig.LogSettings.ConsoleLevel)
assert.True(t, oldConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
states := make(map[string]*model.PluginState)
states["com.mattermost.nps"] = &model.PluginState{Enable: *model.NewBool(false)}
config := model.Config{PasswordSettings: model.PasswordSettings{
Lowercase: model.NewBool(true),
MinimumLength: model.NewInt(15),
}, LogSettings: model.LogSettings{
ConsoleLevel: model.NewString("INFO"),
},
TeamSettings: model.TeamSettings{
ExperimentalDefaultChannels: []string{"another-channel"},
},
PluginSettings: model.PluginSettings{
PluginStates: states,
},
}
_, response, err := client.PatchConfig(&config)
require.NoError(t, err)
updatedConfig, _, err := client.GetConfig()
require.NoError(t, err)
assert.True(t, *updatedConfig.PasswordSettings.Lowercase)
assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
assert.Equal(t, []string{"another-channel"}, updatedConfig.TeamSettings.ExperimentalDefaultChannels)
assert.False(t, updatedConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
assert.Equal(t, "no-cache, no-store, must-revalidate", response.Header.Get("Cache-Control"))
// reset the config
_, _, err = client.UpdateConfig(oldConfig)
require.NoError(t, err)
})
t.Run("should sanitize config", func(t *testing.T) {
config := model.Config{PasswordSettings: model.PasswordSettings{
Symbol: model.NewBool(true),
}}
updatedConfig, _, err := client.PatchConfig(&config)
require.NoError(t, err)
assert.Equal(t, model.FakeSetting, *updatedConfig.SqlSettings.DataSource)
})
t.Run("not allowing to toggle enable uploads for plugin via api", func(t *testing.T) {
config := model.Config{PluginSettings: model.PluginSettings{
EnableUploads: model.NewBool(true),
}}
updatedConfig, resp, err := client.PatchConfig(&config)
if client == th.LocalClient {
require.NoError(t, err)
CheckOKStatus(t, resp)
assert.Equal(t, true, *updatedConfig.PluginSettings.EnableUploads)
} else {
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
})
})
t.Run("Should not be able to modify PluginSettings.MarketplaceURL if EnableUploads is disabled", func(t *testing.T) {
oldURL := "hello.com"
newURL := "new.com"
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = false
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg := th.App.Config().Clone()
*cfg.PluginSettings.MarketplaceURL = newURL
_, _, err := th.SystemAdminClient.PatchConfig(cfg)
require.Error(t, err)
// Allowing uploads
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.EnableUploads = true
*cfg.PluginSettings.MarketplaceURL = oldURL
})
cfg = th.App.Config().Clone()
*cfg.PluginSettings.MarketplaceURL = newURL
cfg, _, err = th.SystemAdminClient.PatchConfig(cfg)
require.NoError(t, err)
assert.Equal(t, newURL, *cfg.PluginSettings.MarketplaceURL)
})
t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
cfg, _, err := th.SystemAdminClient.GetConfig()
require.NoError(t, err)
siteURL := cfg.ServiceSettings.SiteURL
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
// Set the SiteURL
nonEmptyURL := "http://localhost"
config := model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: model.NewString(nonEmptyURL),
},
}
updatedConfig, _, err := th.SystemAdminClient.PatchConfig(&config)
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *updatedConfig.ServiceSettings.SiteURL)
// Check that the Site URL can't be cleared
config = model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: model.NewString(""),
},
}
_, resp, err := th.SystemAdminClient.PatchConfig(&config)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "api.config.update_config.clear_siteurl.app_error")
// Check that the Site URL wasn't cleared
cfg, _, err = th.SystemAdminClient.GetConfig()
require.NoError(t, err)
require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
// Check that sending an empty config returns no error.
_, _, err = th.SystemAdminClient.PatchConfig(&model.Config{})
require.NoError(t, err)
})
}
func TestMigrateConfig(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("LocalClient", func(t *testing.T) {
cfg := &model.Config{}
cfg.SetDefaults()
file, err := json.MarshalIndent(cfg, "", " ")
require.NoError(t, err)
err = os.WriteFile("from.json", file, 0644)
require.NoError(t, err)
defer os.Remove("from.json")
f, err := config.NewStoreFromDSN("from.json", false, nil, false)
require.NoError(t, err)
defer f.RemoveFile("from.json")
_, err = config.NewStoreFromDSN("to.json", false, nil, true)
require.NoError(t, err)
defer f.RemoveFile("to.json")
_, err = th.LocalClient.MigrateConfig("from.json", "to.json")
require.NoError(t, err)
})
}

154
server/channels/api4/cors_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,154 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
)
const (
acAllowOrigin = "Access-Control-Allow-Origin"
acExposeHeaders = "Access-Control-Expose-Headers"
acMaxAge = "Access-Control-Max-Age"
acAllowCredentials = "Access-Control-Allow-Credentials"
acAllowMethods = "Access-Control-Allow-Methods"
acAllowHeaders = "Access-Control-Allow-Headers"
)
func TestCORSRequestHandling(t *testing.T) {
for name, testcase := range map[string]struct {
AllowCorsFrom string
CorsExposedHeaders string
CorsAllowCredentials bool
ModifyRequest func(req *http.Request)
ExpectedAllowOrigin string
ExpectedExposeHeaders string
ExpectedAllowCredentials string
}{
"NoCORS": {
"",
"",
false,
func(req *http.Request) {
},
"",
"",
"",
},
"CORSEnabled": {
"http://somewhere.com",
"",
false,
func(req *http.Request) {
},
"",
"",
"",
},
"CORSEnabledStarOrigin": {
"*",
"",
false,
func(req *http.Request) {
req.Header.Set("Origin", "http://pre-release.mattermost.com")
},
"*",
"",
"",
},
"CORSEnabledStarNoOrigin": { // CORS spec requires this, not a bug.
"*",
"",
false,
func(req *http.Request) {
},
"",
"",
"",
},
"CORSEnabledMatching": {
"http://mattermost.com",
"",
false,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"",
"",
},
"CORSEnabledMultiple": {
"http://spinmint.com http://mattermost.com",
"",
false,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"",
"",
},
"CORSEnabledWithCredentials": {
"http://mattermost.com",
"",
true,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"",
"true",
},
"CORSEnabledWithHeaders": {
"http://mattermost.com",
"x-my-special-header x-blueberry",
true,
func(req *http.Request) {
req.Header.Set("Origin", "http://mattermost.com")
},
"http://mattermost.com",
"X-My-Special-Header, X-Blueberry",
"true",
},
} {
t.Run(name, func(t *testing.T) {
th := SetupConfigWithStoreMock(t, func(cfg *model.Config) {
*cfg.ServiceSettings.AllowCorsFrom = testcase.AllowCorsFrom
*cfg.ServiceSettings.CorsExposedHeaders = testcase.CorsExposedHeaders
*cfg.ServiceSettings.CorsAllowCredentials = testcase.CorsAllowCredentials
})
defer th.TearDown()
licenseStore := mocks.LicenseStore{}
licenseStore.On("Get", "").Return(&model.LicenseRecord{}, nil)
th.App.Srv().Store().(*mocks.Store).On("License").Return(&licenseStore)
port := th.App.Srv().ListenAddr.Port
host := fmt.Sprintf("http://localhost:%v", port)
url := fmt.Sprintf("%v/api/v4/system/ping", host)
req, err := http.NewRequest("GET", url, nil)
require.NoError(t, err)
testcase.ModifyRequest(req)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, testcase.ExpectedAllowOrigin, resp.Header.Get(acAllowOrigin))
assert.Equal(t, testcase.ExpectedExposeHeaders, resp.Header.Get(acExposeHeaders))
assert.Equal(t, "", resp.Header.Get(acMaxAge))
assert.Equal(t, testcase.ExpectedAllowCredentials, resp.Header.Get(acAllowCredentials))
assert.Equal(t, "", resp.Header.Get(acAllowMethods))
assert.Equal(t, "", resp.Header.Get(acAllowHeaders))
})
}
}

497
server/channels/api4/data_retention.go Обычный файл
Просмотреть файл

@@ -0,0 +1,497 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitDataRetention() {
api.BaseRoutes.DataRetention.Handle("/policy", api.APISessionRequired(getGlobalPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(getPolicies)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies_count", api.APISessionRequired(getPoliciesCount)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies", api.APISessionRequired(createPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(getPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(patchPolicy)).Methods("PATCH")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}", api.APISessionRequired(deletePolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(getTeamsForPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(addTeamsToPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams", api.APISessionRequired(removeTeamsFromPolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/teams/search", api.APISessionRequired(searchTeamsInPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(getChannelsForPolicy)).Methods("GET")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(addChannelsToPolicy)).Methods("POST")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels", api.APISessionRequired(removeChannelsFromPolicy)).Methods("DELETE")
api.BaseRoutes.DataRetention.Handle("/policies/{policy_id:[A-Za-z0-9]+}/channels/search", api.APISessionRequired(searchChannelsInPolicy)).Methods("POST")
api.BaseRoutes.User.Handle("/data_retention/team_policies", api.APISessionRequired(getTeamPoliciesForUser)).Methods("GET")
api.BaseRoutes.User.Handle("/data_retention/channel_policies", api.APISessionRequired(getChannelPoliciesForUser)).Methods("GET")
}
func getGlobalPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
// No permission check required.
policy, appErr := c.App.GetGlobalRetentionPolicy()
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("getGlobalPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getPolicies(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
limit := c.Params.PerPage
offset := c.Params.Page * limit
policies, appErr := c.App.GetRetentionPolicies(offset, limit)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policies)
if err != nil {
c.Err = model.NewAppError("getPolicies", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getPoliciesCount(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
count, appErr := c.App.GetRetentionPoliciesCount()
if appErr != nil {
c.Err = appErr
return
}
body := struct {
TotalCount int64 `json:"total_count"`
}{count}
err := json.NewEncoder(w).Encode(body)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func getPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policy, appErr := c.App.GetRetentionPolicy(c.Params.PolicyId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("getPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func createPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
var policy model.RetentionPolicyWithTeamAndChannelIDs
if jsonErr := json.NewDecoder(r.Body).Decode(&policy); jsonErr != nil {
c.SetInvalidParamWithErr("policy", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "policy", &policy)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
newPolicy, appErr := c.App.CreateRetentionPolicy(&policy)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newPolicy)
auditRec.AddEventObjectType("policy")
js, err := json.Marshal(newPolicy)
if err != nil {
c.Err = model.NewAppError("createPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
w.Write(js)
}
func patchPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
var patch model.RetentionPolicyWithTeamAndChannelIDs
if jsonErr := json.NewDecoder(r.Body).Decode(&patch); jsonErr != nil {
c.SetInvalidParamWithErr("policy", jsonErr)
return
}
c.RequirePolicyId()
patch.ID = c.Params.PolicyId
auditRec := c.MakeAuditRecord("patchPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "patch", &patch)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
policy, appErr := c.App.PatchRetentionPolicy(&patch)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(policy)
auditRec.AddEventObjectType("retention_policy")
js, err := json.Marshal(policy)
if err != nil {
c.Err = model.NewAppError("patchPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.Write(js)
}
func deletePolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
auditRec := c.MakeAuditRecord("deletePolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.DeleteRetentionPolicy(policyId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policyId := c.Params.PolicyId
limit := c.Params.PerPage
offset := c.Params.Page * limit
teams, appErr := c.App.GetTeamsForRetentionPolicy(policyId, offset, limit)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("Api4.getTeamsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func searchTeamsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
var props model.TeamSearch
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("team_search", err)
return
}
props.PolicyID = model.NewString(c.Params.PolicyId)
props.IncludePolicyID = model.NewBool(true)
teams, _, appErr := c.App.SearchAllTeams(&props)
if appErr != nil {
c.Err = appErr
return
}
c.App.SanitizeTeams(*c.AppContext.Session(), teams)
js, err := json.Marshal(teams)
if err != nil {
c.Err = model.NewAppError("searchTeamsInPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var teamIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("team_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("addTeamsToPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.AddTeamsToRetentionPolicy(policyId, teamIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var teamIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("team_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("removeTeamsFromPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "team_ids", teamIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.RemoveTeamsFromRetentionPolicy(policyId, teamIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getChannelsForPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
c.RequirePolicyId()
policyId := c.Params.PolicyId
limit := c.Params.PerPage
offset := c.Params.Page * limit
channels, appErr := c.App.GetChannelsForRetentionPolicy(policyId, offset, limit)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(channels)
if err != nil {
c.Err = model.NewAppError("Api4.getChannelsForPolicy", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
var props *model.ChannelSearch
err := json.NewDecoder(r.Body).Decode(&props)
if err != nil {
c.SetInvalidParamWithErr("channel_search", err)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleReadComplianceDataRetentionPolicy)
return
}
opts := model.ChannelSearchOpts{
PolicyID: c.Params.PolicyId,
IncludePolicyID: true,
Deleted: props.Deleted,
IncludeDeleted: props.IncludeDeleted,
Public: props.Public,
Private: props.Private,
TeamIds: props.TeamIds,
}
channels, _, appErr := c.App.SearchAllChannels(c.AppContext, props.Term, opts)
if appErr != nil {
c.Err = appErr
return
}
channelsJSON, jsonErr := json.Marshal(channels)
if jsonErr != nil {
c.Err = model.NewAppError("searchChannelsInPolicy", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(channelsJSON)
}
func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var channelIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("channel_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("addChannelsToPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.AddChannelsToRetentionPolicy(policyId, channelIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePolicyId()
policyId := c.Params.PolicyId
var channelIDs []string
jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
if jsonErr != nil {
c.SetInvalidParamWithErr("channel_ids", jsonErr)
return
}
auditRec := c.MakeAuditRecord("removeChannelsFromPolicy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "policy_id", policyId)
audit.AddEventParameter(auditRec, "channel_ids", channelIDs)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteComplianceDataRetentionPolicy) {
c.SetPermissionError(model.PermissionSysconsoleWriteComplianceDataRetentionPolicy)
return
}
err := c.App.RemoveChannelsFromRetentionPolicy(policyId, channelIDs)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getTeamPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userID := c.Params.UserId
limit := c.Params.PerPage
offset := c.Params.Page * limit
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
policies, err := c.App.GetTeamPoliciesForUser(userID, offset, limit)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(policies)
if jsonErr != nil {
c.Err = model.NewAppError("getTeamPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
}
func getChannelPoliciesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
userID := c.Params.UserId
limit := c.Params.PerPage
offset := c.Params.Page * limit
if userID != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
policies, err := c.App.GetChannelPoliciesForUser(userID, offset, limit)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(policies)
if jsonErr != nil {
c.Err = model.NewAppError("getChannelPoliciesForUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}
w.Write(js)
}

19
server/channels/api4/data_retention_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDataRetentionGetPolicy(t *testing.T) {
th := Setup(t)
defer th.TearDown()
_, resp, err := th.Client.GetDataRetentionPolicy()
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}

145
server/channels/api4/drafts.go Обычный файл
Просмотреть файл

@@ -0,0 +1,145 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitDrafts() {
api.BaseRoutes.Drafts.Handle("", api.APISessionRequired(upsertDraft)).Methods("POST")
api.BaseRoutes.TeamForUser.Handle("/drafts", api.APISessionRequired(getDrafts)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/drafts/{thread_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteDraft)).Methods("DELETE")
api.BaseRoutes.ChannelForUser.Handle("/drafts", api.APISessionRequired(deleteDraft)).Methods("DELETE")
}
func upsertDraft(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("upsertDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
var draft model.Draft
if jsonErr := json.NewDecoder(r.Body).Decode(&draft); jsonErr != nil {
c.SetInvalidParam("draft")
return
}
draft.DeleteAt = 0
draft.UserId = c.AppContext.Session().UserId
connectionID := r.Header.Get(model.ConnectionId)
hasPermission := false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), draft.ChannelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := c.App.GetChannel(c.AppContext, draft.ChannelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
dt, err := c.App.UpsertDraft(c.AppContext, &draft, connectionID)
if err != nil {
c.Err = err
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(dt); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func getDrafts(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("getDrafts", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
hasPermission := false
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
hasPermission = true
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
drafts, err := c.App.GetDraftsForUser(c.AppContext.Session().UserId, c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(drafts); err != nil {
mlog.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteDraft(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.AllowSyncedDrafts {
c.Err = model.NewAppError("deleteDraft", "api.drafts.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
rootID := ""
connectionID := r.Header.Get(model.ConnectionId)
if c.Params.ThreadId != "" {
rootID = c.Params.ThreadId
}
userID := c.AppContext.Session().UserId
channelID := c.Params.ChannelId
draft, err := c.App.GetDraft(userID, channelID, rootID)
if err != nil {
switch {
case err.StatusCode == http.StatusNotFound:
// If the draft doesn't exist in the server, we don't need to delete.
ReturnStatusOK(w)
default:
c.Err = err
}
return
}
if c.AppContext.Session().UserId != draft.UserId {
c.SetPermissionError(model.PermissionDeletePost)
return
}
if _, err := c.App.DeleteDraft(userID, channelID, rootID, connectionID); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}

228
server/channels/api4/drafts_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,228 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/testutils"
)
func TestUpsertDraft(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
th := Setup(t).InitBasic()
defer th.TearDown()
// set config
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
client := th.Client
channel := th.BasicChannel
user := th.BasicUser
draft := &model.Draft{
CreateAt: 12345,
UpdateAt: 12345,
UserId: user.Id,
ChannelId: channel.Id,
Message: "original",
}
// try to upsert draft
draftResp, _, err := client.UpsertDraft(draft)
require.NoError(t, err)
assert.Equal(t, draft.UserId, draftResp.UserId)
assert.Equal(t, draft.Message, draftResp.Message)
assert.Equal(t, draft.ChannelId, draftResp.ChannelId)
// upload file
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(sent, channel.Id, "test.png")
require.NoError(t, err)
draftWithFiles := draft
draftWithFiles.FileIds = []string{fileResp.FileInfos[0].Id}
// try to upsert draft with file
draftResp, _, err = client.UpsertDraft(draftWithFiles)
require.NoError(t, err)
assert.Equal(t, draftWithFiles.UserId, draftResp.UserId)
assert.Equal(t, draftWithFiles.Message, draftResp.Message)
assert.Equal(t, draftWithFiles.ChannelId, draftResp.ChannelId)
assert.ElementsMatch(t, draftWithFiles.FileIds, draftResp.FileIds)
// try to upsert draft for invalid channel
draftInvalidChannel := draft
draftInvalidChannel.ChannelId = "12345"
_, resp, err := client.UpsertDraft(draft)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// try to upsert draft without config setting set to true
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
_, resp, err = client.UpsertDraft(draft)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestGetDrafts(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
client := th.Client
channel1 := th.BasicChannel
channel2 := th.BasicChannel2
user := th.BasicUser
team := th.BasicTeam
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel1.Id,
Message: "draft1",
}
draft2 := &model.Draft{
CreateAt: 11111,
UpdateAt: 32222,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
}
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
// upsert draft1
_, _, err := client.UpsertDraft(draft1)
require.NoError(t, err)
// upsert draft2
_, _, err = client.UpsertDraft(draft2)
require.NoError(t, err)
// try to get drafts
draftResp, _, err := client.GetDrafts(user.Id, team.Id)
require.NoError(t, err)
assert.Equal(t, draft2.UserId, draftResp[0].UserId)
assert.Equal(t, draft2.Message, draftResp[0].Message)
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
assert.Equal(t, draft1.UserId, draftResp[1].UserId)
assert.Equal(t, draft1.Message, draftResp[1].Message)
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
assert.Len(t, draftResp, 2)
// try to get drafts on invalid team
_, resp, err := client.GetDrafts(user.Id, "12345")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// try to get drafts when config is turned off
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
_, resp, err = client.GetDrafts(user.Id, team.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestDeleteDraft(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
client := th.Client
channel1 := th.BasicChannel
channel2 := th.BasicChannel2
user := th.BasicUser
team := th.BasicTeam
draft1 := &model.Draft{
CreateAt: 00001,
UpdateAt: 00001,
UserId: user.Id,
ChannelId: channel1.Id,
Message: "draft1",
RootId: "",
}
draft2 := &model.Draft{
CreateAt: 11111,
UpdateAt: 32222,
UserId: user.Id,
ChannelId: channel2.Id,
Message: "draft2",
RootId: model.NewId(),
}
// upsert draft1
_, _, err := client.UpsertDraft(draft1)
require.NoError(t, err)
// upsert draft2
_, _, err = client.UpsertDraft(draft2)
require.NoError(t, err)
//get drafts
draftResp, _, err := client.GetDrafts(user.Id, team.Id)
require.NoError(t, err)
assert.Equal(t, draft2.UserId, draftResp[0].UserId)
assert.Equal(t, draft2.Message, draftResp[0].Message)
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
assert.Equal(t, draft1.UserId, draftResp[1].UserId)
assert.Equal(t, draft1.Message, draftResp[1].Message)
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
// try to delete draft1
_, _, err = client.DeleteDraft(user.Id, channel1.Id, draft1.RootId)
require.NoError(t, err)
//get drafts
draftResp, _, err = client.GetDrafts(user.Id, team.Id)
require.NoError(t, err)
assert.Equal(t, draft2.UserId, draftResp[0].UserId)
assert.Equal(t, draft2.Message, draftResp[0].Message)
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
assert.Len(t, draftResp, 1)
}

82
server/channels/api4/elasticsearch.go Обычный файл
Просмотреть файл

@@ -0,0 +1,82 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitElasticsearch() {
api.BaseRoutes.Elasticsearch.Handle("/test", api.APISessionRequired(testElasticsearch)).Methods("POST")
api.BaseRoutes.Elasticsearch.Handle("/purge_indexes", api.APISessionRequired(purgeElasticsearchIndexes)).Methods("POST")
}
func testElasticsearch(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
c.Logger.Warn("Error decoding config.", mlog.Err(err))
}
if cfg == nil {
cfg = c.App.Config()
}
// we set BulkIndexingTimeWindowSeconds to a random value to avoid failing on the nil check
// TODO: remove this hack once we remove BulkIndexingTimeWindowSeconds from the config.
if cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds == nil {
cfg.ElasticsearchSettings.BulkIndexingTimeWindowSeconds = model.NewInt(0)
}
if checkHasNilFields(&cfg.ElasticsearchSettings) {
c.Err = model.NewAppError("testElasticsearch", "api.elasticsearch.test_elasticsearch_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
// PERMISSION_TEST_ELASTICSEARCH is an ancillary permission of PERMISSION_SYSCONSOLE_WRITE_ENVIRONMENT_ELASTICSEARCH,
// which should prevent read-only managers from password sniffing
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestElasticsearch) {
c.SetPermissionError(model.PermissionTestElasticsearch)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testElasticsearch", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.TestElasticsearch(cfg); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func purgeElasticsearchIndexes(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("purgeElasticsearchIndexes", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionPurgeElasticsearchIndexes) {
c.SetPermissionError(model.PermissionPurgeElasticsearchIndexes)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("purgeElasticsearchIndexes", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.PurgeElasticsearchIndexes(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}

80
server/channels/api4/elasticsearch_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestElasticsearchTest(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("as system user", func(t *testing.T) {
resp, err := th.Client.TestElasticsearch()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("as system admin", func(t *testing.T) {
resp, err := th.SystemAdminClient.TestElasticsearch()
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("invalid config", func(t *testing.T) {
cfg := &model.Config{}
cfg.SetDefaults()
cfg.ElasticsearchSettings.Password = nil
data, err := json.Marshal(cfg)
require.NoError(t, err)
resp, err := th.SystemAdminClient.DoAPIPost("/elasticsearch/test", string(data))
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("as restricted system admin", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ElasticsearchSettings.SetDefaults()
*cfg.ExperimentalSettings.RestrictSystemAdmin = true
})
resp, err := th.SystemAdminClient.TestElasticsearch()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}
func TestElasticsearchPurgeIndexes(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("as system user", func(t *testing.T) {
resp, err := th.Client.PurgeElasticsearchIndexes()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("as system admin", func(t *testing.T) {
resp, err := th.SystemAdminClient.PurgeElasticsearchIndexes()
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
t.Run("as restricted system admin", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
resp, err := th.SystemAdminClient.PurgeElasticsearchIndexes()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
}

297
server/channels/api4/emoji.go Обычный файл
Просмотреть файл

@@ -0,0 +1,297 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
EmojiMaxAutocompleteItems = 100
)
func (api *API) InitEmoji() {
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji)).Methods("POST")
api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods("GET")
api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods("POST")
api.BaseRoutes.Emojis.Handle("/autocomplete", api.APISessionRequired(autocompleteEmojis)).Methods("GET")
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(deleteEmoji)).Methods("DELETE")
api.BaseRoutes.Emoji.Handle("", api.APISessionRequired(getEmoji)).Methods("GET")
api.BaseRoutes.EmojiByName.Handle("", api.APISessionRequired(getEmojiByName)).Methods("GET")
api.BaseRoutes.Emoji.Handle("/image", api.APISessionRequiredTrustRequester(getEmojiImage)).Methods("GET")
}
func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("createEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > app.MaxEmojiFileSize {
c.Err = model.NewAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(app.MaxEmojiFileSize); err != nil {
c.Err = model.NewAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("createEmoji", audit.Fail)
defer c.LogAuditRec(auditRec)
// Allow any user with CREATE_EMOJIS permission at Team level to create emojis at system level
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionCreateEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreateEmojis)
return
}
}
m := r.MultipartForm
props := m.Value
if len(props["emoji"]) == 0 {
c.SetInvalidParam("emoji")
return
}
var emoji model.Emoji
if jsonErr := json.Unmarshal([]byte(props["emoji"][0]), &emoji); jsonErr != nil {
c.SetInvalidParam("emoji")
return
}
auditRec.AddEventResultState(&emoji)
auditRec.AddEventObjectType("emoji")
newEmoji, err := c.App.CreateEmoji(c.AppContext, c.AppContext.Session().UserId, &emoji, m)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(newEmoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiList(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
sort := r.URL.Query().Get("sort")
if sort != "" && sort != model.EmojiSortByName {
c.SetInvalidURLParam("sort")
return
}
listEmoji, err := c.App.GetEmojiList(c.AppContext, c.Params.Page, c.Params.PerPage, sort)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(listEmoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteEmoji", audit.Fail)
defer c.LogAuditRec(auditRec)
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
if err != nil {
audit.AddEventParameter(auditRec, "emoji_id", c.Params.EmojiId)
c.Err = err
return
}
auditRec.AddEventPriorState(emoji)
auditRec.AddEventObjectType("emoji")
// Allow any user with DELETE_EMOJIS permission at Team level to delete emojis at system level
memberships, err := c.App.GetTeamMembersForUser(c.AppContext.Session().UserId, "", true)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionDeleteEmojis)
return
}
}
if c.AppContext.Session().UserId != emoji.CreatorId {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDeleteOthersEmojis) {
hasPermission := false
for _, membership := range memberships {
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), membership.TeamId, model.PermissionDeleteOthersEmojis) {
hasPermission = true
break
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionDeleteOthersEmojis)
return
}
}
}
err = c.App.DeleteEmoji(c.AppContext, emoji)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
emoji, err := c.App.GetEmoji(c.AppContext, c.Params.EmojiId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiByName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiName()
if c.Err != nil {
return
}
emoji, err := c.App.GetEmojiByName(c.AppContext, c.Params.EmojiName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emoji); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireEmojiId()
if c.Err != nil {
return
}
if !*c.App.Config().ServiceSettings.EnableCustomEmoji {
c.Err = model.NewAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
image, imageType, err := c.App.GetEmojiImage(c.AppContext, c.Params.EmojiId)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "image/"+imageType)
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Write(image)
}
func searchEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
var emojiSearch model.EmojiSearch
if jsonErr := json.NewDecoder(r.Body).Decode(&emojiSearch); jsonErr != nil {
c.SetInvalidParamWithErr("term", jsonErr)
return
}
if emojiSearch.Term == "" {
c.SetInvalidParam("term")
return
}
emojis, err := c.App.SearchEmoji(c.AppContext, emojiSearch.Term, emojiSearch.PrefixOnly, web.PerPageMaximum)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emojis); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func autocompleteEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
c.SetInvalidURLParam("name")
return
}
emojis, err := c.App.SearchEmoji(c.AppContext, name, true, EmojiMaxAutocompleteItems)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(emojis); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}

731
server/channels/api4/emoji_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,731 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"image"
_ "image/gif"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
func TestCreateEmoji(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = false })
defaultRolePermissions := th.SaveDefaultRolePermissions()
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
}()
// constants to be used along with checkEmojiFile
emojiWidth := app.MaxEmojiWidth
emojiHeight := app.MaxEmojiHeight * 2
// check that emoji gets resized correctly, respecting proportions, and is of expected type
checkEmojiFile := func(id, expectedImageType string) {
path, _ := fileutils.FindDir("data")
file, fileErr := os.Open(filepath.Join(path, "/emoji/"+id+"/image"))
require.NoError(t, fileErr)
defer file.Close()
config, imageType, err := image.DecodeConfig(file)
require.NoError(t, err)
require.Equal(t, expectedImageType, imageType)
require.Equal(t, emojiWidth/2, config.Width)
require.Equal(t, emojiHeight/2, config.Height)
}
emoji := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
// try to create an emoji when they're disabled
_, resp, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
// enable emoji creation for next cases
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
// try to create a valid gif emoji when they're enabled
newEmoji, _, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, emojiWidth, emojiHeight), "image.gif")
require.NoError(t, err)
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
checkEmojiFile(newEmoji.Id, "gif")
// try to create an emoji with a duplicate name
emoji2 := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: newEmoji.Name,
}
_, resp, err = client.CreateEmoji(emoji2, utils.CreateTestGif(t, 10, 10), "image.gif")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "api.emoji.create.duplicate.app_error")
// try to create a valid animated gif emoji
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, emojiWidth, emojiHeight, 10), "image.gif")
require.NoError(t, err)
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
checkEmojiFile(newEmoji.Id, "gif")
// try to create a valid webp emoji
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
path, _ := fileutils.FindDir("tests")
bytes, err := os.ReadFile(filepath.Join(path, "testwebp.webp"))
require.NoError(t, err)
newEmoji, _, err = client.CreateEmoji(emoji, bytes, "image.webp")
require.NoError(t, err)
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
checkEmojiFile(newEmoji.Id, "png") // emoji must be converted from webp to png
// try to create a valid jpeg emoji
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestJpeg(t, emojiWidth, emojiHeight), "image.jpeg")
require.NoError(t, err)
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
checkEmojiFile(newEmoji.Id, "png") // emoji must be converted from jpeg to png
// try to create a valid png emoji
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestPng(t, emojiWidth, emojiHeight), "image.png")
require.NoError(t, err)
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
checkEmojiFile(newEmoji.Id, "png")
// try to create an emoji that's too wide
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 1000, 10), "image.gif")
require.NoError(t, err)
require.Equal(t, newEmoji.Name, emoji.Name, "create with wrong name")
// try to create an emoji that's too wide
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
_, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, app.MaxEmojiOriginalWidth+1), "image.gif")
require.Error(t, err, "should fail - emoji is too wide")
// try to create an emoji that's too tall
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
_, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, app.MaxEmojiOriginalHeight+1, 10), "image.gif")
require.Error(t, err, "should fail - emoji is too tall")
// try to create an emoji that's too large
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
_, _, err = client.CreateEmoji(emoji, utils.CreateTestAnimatedGif(t, 100, 100, 10000), "image.gif")
require.Error(t, err, "should fail - emoji is too big")
// try to create an emoji with data that isn't an image
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
_, resp, err = client.CreateEmoji(emoji, make([]byte, 100), "image.gif")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
CheckErrorID(t, err, "api.emoji.upload.image.app_error")
// try to create an emoji as another user
emoji = &model.Emoji{
CreatorId: th.BasicUser2.Id,
Name: model.NewId(),
}
_, resp, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// try to create an emoji without permissions
th.RemovePermissionFromRole(model.PermissionCreateEmojis.Id, model.SystemUserRoleId)
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
_, resp, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// create an emoji with permissions in one team
th.AddPermissionToRole(model.PermissionCreateEmojis.Id, model.TeamUserRoleId)
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
_, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
}
func TestGetEmojiList(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
emojis := []*model.Emoji{
{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
},
{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
},
{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
},
}
for idx, emoji := range emojis {
newEmoji, _, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
emojis[idx] = newEmoji
}
listEmoji, _, err := client.GetEmojiList(0, 100)
require.NoError(t, err)
for _, emoji := range emojis {
found := false
for _, savedEmoji := range listEmoji {
if emoji.Id == savedEmoji.Id {
found = true
break
}
}
require.Truef(t, found, "failed to get emoji with id %v, %v", emoji.Id, len(listEmoji))
}
_, err = client.DeleteEmoji(emojis[0].Id)
require.NoError(t, err)
listEmoji, _, err = client.GetEmojiList(0, 100)
require.NoError(t, err)
found := false
for _, savedEmoji := range listEmoji {
if savedEmoji.Id == emojis[0].Id {
found = true
break
}
}
require.Falsef(t, found, "should not get a deleted emoji %v", emojis[0].Id)
listEmoji, _, err = client.GetEmojiList(0, 1)
require.NoError(t, err)
require.Len(t, listEmoji, 1, "should only return 1")
listEmoji, _, err = client.GetSortedEmojiList(0, 100, model.EmojiSortByName)
require.NoError(t, err)
require.Greater(t, len(listEmoji), 0, "should return more than 0")
}
func TestDeleteEmoji(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
defaultRolePermissions := th.SaveDefaultRolePermissions()
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
}()
emoji := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
_, err = client.DeleteEmoji(newEmoji.Id)
require.NoError(t, err)
_, _, err = client.GetEmoji(newEmoji.Id)
require.Error(t, err, "expected error fetching deleted emoji")
//Admin can delete other users emoji
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
_, err = th.SystemAdminClient.DeleteEmoji(newEmoji.Id)
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetEmoji(newEmoji.Id)
require.Error(t, err, "expected error fetching deleted emoji")
// Try to delete just deleted emoji
resp, err := client.DeleteEmoji(newEmoji.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
//Try to delete non-existing emoji
resp, err = client.DeleteEmoji(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
//Try to delete without Id
resp, err = client.DeleteEmoji("")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
//Try to delete my custom emoji without permissions
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
resp, err = client.DeleteEmoji(newEmoji.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
//Try to delete other user's custom emoji without DELETE_EMOJIS permissions
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
th.AddPermissionToRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
client.Logout()
th.LoginBasic2()
resp, err = client.DeleteEmoji(newEmoji.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.RemovePermissionFromRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
client.Logout()
th.LoginBasic()
//Try to delete other user's custom emoji without DELETE_OTHERS_EMOJIS permissions
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
client.Logout()
th.LoginBasic2()
resp, err = client.DeleteEmoji(newEmoji.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
th.LoginBasic()
//Try to delete other user's custom emoji with permissions
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
th.AddPermissionToRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
client.Logout()
th.LoginBasic2()
_, err = client.DeleteEmoji(newEmoji.Id)
require.NoError(t, err)
client.Logout()
th.LoginBasic()
//Try to delete my custom emoji with permissions at team level
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.TeamUserRoleId)
_, err = client.DeleteEmoji(newEmoji.Id)
require.NoError(t, err)
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.TeamUserRoleId)
//Try to delete other user's custom emoji with permissions at team level
emoji = &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err = client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
th.RemovePermissionFromRole(model.PermissionDeleteEmojis.Id, model.SystemUserRoleId)
th.RemovePermissionFromRole(model.PermissionDeleteOthersEmojis.Id, model.SystemUserRoleId)
th.AddPermissionToRole(model.PermissionDeleteEmojis.Id, model.TeamUserRoleId)
th.AddPermissionToRole(model.PermissionDeleteOthersEmojis.Id, model.TeamUserRoleId)
client.Logout()
th.LoginBasic2()
_, err = client.DeleteEmoji(newEmoji.Id)
require.NoError(t, err)
}
func TestGetEmoji(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
EnableCustomEmoji := *th.App.Config().ServiceSettings.EnableCustomEmoji
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
emoji := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
emoji, _, err = client.GetEmoji(newEmoji.Id)
require.NoError(t, err)
require.Equal(t, newEmoji.Id, emoji.Id, "wrong emoji was returned")
_, resp, err := client.GetEmoji(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}
func TestGetEmojiByName(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
emoji := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
newEmoji, _, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
emoji, _, err = client.GetEmojiByName(newEmoji.Name)
require.NoError(t, err)
assert.Equal(t, newEmoji.Name, emoji.Name)
_, resp, err := client.GetEmojiByName(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
client.Logout()
_, resp, err = client.GetEmojiByName(newEmoji.Name)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetEmojiImage(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
emoji1 := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
emoji1, _, err := client.CreateEmoji(emoji1, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = false })
_, resp, err := client.GetEmojiImage(emoji1.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
CheckErrorID(t, err, "api.emoji.disabled.app_error")
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.DriverName = "local" })
emojiImage, _, err := client.GetEmojiImage(emoji1.Id)
require.NoError(t, err)
require.Greater(t, len(emojiImage), 0, "should return the image")
_, imageType, err := image.DecodeConfig(bytes.NewReader(emojiImage))
require.NoError(t, err)
require.Equal(t, imageType, "gif", "expected gif")
emoji2 := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
emoji2, _, err = client.CreateEmoji(emoji2, utils.CreateTestAnimatedGif(t, 10, 10, 10), "image.gif")
require.NoError(t, err)
emojiImage, _, err = client.GetEmojiImage(emoji2.Id)
require.NoError(t, err)
require.Greater(t, len(emojiImage), 0, "no image returned")
_, imageType, err = image.DecodeConfig(bytes.NewReader(emojiImage))
require.NoError(t, err, "unable to identify received image")
require.Equal(t, imageType, "gif", "expected gif")
emoji3 := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
emoji3, _, err = client.CreateEmoji(emoji3, utils.CreateTestJpeg(t, 10, 10), "image.jpg")
require.NoError(t, err)
emojiImage, _, err = client.GetEmojiImage(emoji3.Id)
require.NoError(t, err)
require.Greater(t, len(emojiImage), 0, "no image returned")
_, imageType, err = image.DecodeConfig(bytes.NewReader(emojiImage))
require.NoError(t, err, "unable to identify received image")
require.Equal(t, imageType, "jpeg", "expected jpeg")
emoji4 := &model.Emoji{
CreatorId: th.BasicUser.Id,
Name: model.NewId(),
}
emoji4, _, err = client.CreateEmoji(emoji4, utils.CreateTestPng(t, 10, 10), "image.png")
require.NoError(t, err)
emojiImage, _, err = client.GetEmojiImage(emoji4.Id)
require.NoError(t, err)
require.Greater(t, len(emojiImage), 0, "no image returned")
_, imageType, err = image.DecodeConfig(bytes.NewReader(emojiImage))
require.NoError(t, err, "unable to identify received image")
require.Equal(t, imageType, "png", "expected png")
_, err = client.DeleteEmoji(emoji4.Id)
require.NoError(t, err)
_, resp, err = client.GetEmojiImage(emoji4.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, resp, err = client.GetEmojiImage(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, resp, err = client.GetEmojiImage("")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}
func TestSearchEmoji(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
searchTerm1 := model.NewId()
searchTerm2 := model.NewId()
emojis := []*model.Emoji{
{
CreatorId: th.BasicUser.Id,
Name: searchTerm1,
},
{
CreatorId: th.BasicUser.Id,
Name: "blargh_" + searchTerm2,
},
}
for idx, emoji := range emojis {
newEmoji, _, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
emojis[idx] = newEmoji
}
search := &model.EmojiSearch{Term: searchTerm1}
remojis, resp, err := client.SearchEmoji(search)
require.NoError(t, err)
CheckOKStatus(t, resp)
found := false
for _, e := range remojis {
if e.Name == emojis[0].Name {
found = true
}
}
assert.True(t, found)
search.Term = searchTerm2
search.PrefixOnly = true
remojis, resp, err = client.SearchEmoji(search)
require.NoError(t, err)
CheckOKStatus(t, resp)
found = false
for _, e := range remojis {
if e.Name == emojis[1].Name {
found = true
}
}
assert.False(t, found)
search.PrefixOnly = false
remojis, resp, err = client.SearchEmoji(search)
require.NoError(t, err)
CheckOKStatus(t, resp)
found = false
for _, e := range remojis {
if e.Name == emojis[1].Name {
found = true
}
}
assert.True(t, found)
search.Term = ""
_, resp, err = client.SearchEmoji(search)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
client.Logout()
_, resp, err = client.SearchEmoji(search)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestAutocompleteEmoji(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
searchTerm1 := model.NewId()
emojis := []*model.Emoji{
{
CreatorId: th.BasicUser.Id,
Name: searchTerm1,
},
{
CreatorId: th.BasicUser.Id,
Name: "blargh_" + searchTerm1,
},
}
for idx, emoji := range emojis {
newEmoji, _, err := client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
require.NoError(t, err)
emojis[idx] = newEmoji
}
remojis, resp, err := client.AutocompleteEmoji(searchTerm1, "")
require.NoError(t, err)
CheckOKStatus(t, resp)
found1 := false
found2 := false
for _, e := range remojis {
if e.Name == emojis[0].Name {
found1 = true
}
if e.Name == emojis[1].Name {
found2 = true
}
}
assert.True(t, found1)
assert.False(t, found2)
_, resp, err = client.AutocompleteEmoji("", "")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
client.Logout()
_, resp, err = client.AutocompleteEmoji(searchTerm1, "")
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}

86
server/channels/api4/export.go Обычный файл
Просмотреть файл

@@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"path/filepath"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitExport() {
api.BaseRoutes.Exports.Handle("", api.APISessionRequired(listExports)).Methods("GET")
api.BaseRoutes.Export.Handle("", api.APISessionRequired(deleteExport)).Methods("DELETE")
api.BaseRoutes.Export.Handle("", api.APISessionRequired(downloadExport)).Methods("GET")
}
func listExports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
exports, appErr := c.App.ListExports()
if appErr != nil {
c.Err = appErr
return
}
data, err := json.Marshal(exports)
if err != nil {
c.Err = model.NewAppError("listImports", "app.export.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(data)
}
func deleteExport(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("deleteExport", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "export_name", c.Params.ExportName)
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if err := c.App.DeleteExport(c.Params.ExportName); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func downloadExport(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
filePath := filepath.Join(*c.App.Config().ExportSettings.Directory, c.Params.ExportName)
if ok, err := c.App.FileExists(filePath); err != nil {
c.Err = err
return
} else if !ok {
c.Err = model.NewAppError("downloadExport", "api.export.export_not_found.app_error", nil, "", http.StatusNotFound)
return
}
file, err := c.App.FileReader(filePath)
if err != nil {
c.Err = err
return
}
defer file.Close()
w.Header().Set("Content-Type", "application/zip")
http.ServeContent(w, r, c.Params.ExportName, time.Time{}, file)
}

10
server/channels/api4/export_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitExportLocal() {
api.BaseRoutes.Exports.Handle("", api.APILocal(listExports)).Methods("GET")
api.BaseRoutes.Export.Handle("", api.APILocal(deleteExport)).Methods("DELETE")
api.BaseRoutes.Export.Handle("", api.APILocal(downloadExport)).Methods("GET")
}

209
server/channels/api4/export_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,209 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/stretchr/testify/require"
)
func TestListExports(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("no permissions", func(t *testing.T) {
exports, _, err := th.Client.ListExports()
require.Error(t, err)
CheckErrorID(t, err, "api.context.permissions.app_error")
require.Nil(t, exports)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
exports, _, err := c.ListExports()
require.NoError(t, err)
require.Empty(t, exports)
}, "no exports")
dataDir, found := fileutils.FindDir("data")
require.True(t, found)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
err := os.Mkdir(exportDir, 0700)
require.NoError(t, err)
defer os.RemoveAll(exportDir)
f, err := os.Create(filepath.Join(exportDir, "export.zip"))
require.NoError(t, err)
f.Close()
exports, _, err := c.ListExports()
require.NoError(t, err)
require.Len(t, exports, 1)
require.Equal(t, exports[0], "export.zip")
}, "expected exports")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
value := *th.App.Config().ExportSettings.Directory
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExportSettings.Directory = value + "new" })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExportSettings.Directory = value })
exportDir := filepath.Join(dataDir, value+"new")
err := os.Mkdir(exportDir, 0700)
require.NoError(t, err)
defer os.RemoveAll(exportDir)
exports, _, err := c.ListExports()
require.NoError(t, err)
require.Empty(t, exports)
f, err := os.Create(filepath.Join(exportDir, "export.zip"))
require.NoError(t, err)
f.Close()
exports, _, err = c.ListExports()
require.NoError(t, err)
require.Len(t, exports, 1)
require.Equal(t, "export.zip", exports[0])
}, "change export directory")
}
func TestDeleteExport(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("no permissions", func(t *testing.T) {
_, err := th.Client.DeleteExport("export.zip")
require.Error(t, err)
CheckErrorID(t, err, "api.context.permissions.app_error")
})
dataDir, found := fileutils.FindDir("data")
require.True(t, found)
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
err := os.Mkdir(exportDir, 0700)
require.NoError(t, err)
defer os.RemoveAll(exportDir)
exportName := "export.zip"
f, err := os.Create(filepath.Join(exportDir, exportName))
require.NoError(t, err)
f.Close()
exports, _, err := c.ListExports()
require.NoError(t, err)
require.Len(t, exports, 1)
require.Equal(t, exports[0], exportName)
_, err = c.DeleteExport(exportName)
require.NoError(t, err)
exports, _, err = c.ListExports()
require.NoError(t, err)
require.Empty(t, exports)
// verify idempotence
_, err = c.DeleteExport(exportName)
require.NoError(t, err)
}, "successfully delete export")
}
func TestDownloadExport(t *testing.T) {
th := Setup(t)
defer th.TearDown()
t.Run("no permissions", func(t *testing.T) {
var buf bytes.Buffer
n, _, err := th.Client.DownloadExport("export.zip", &buf, 0)
require.Error(t, err)
CheckErrorID(t, err, "api.context.permissions.app_error")
require.Zero(t, n)
})
dataDir, found := fileutils.FindDir("data")
require.True(t, found)
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
var buf bytes.Buffer
n, _, err := c.DownloadExport("export.zip", &buf, 0)
require.Error(t, err)
CheckErrorID(t, err, "api.export.export_not_found.app_error")
require.Zero(t, n)
}, "not found")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
err := os.Mkdir(exportDir, 0700)
require.NoError(t, err)
defer os.RemoveAll(exportDir)
data := randomBytes(t, 1024*1024)
var buf bytes.Buffer
exportName := "export.zip"
err = os.WriteFile(filepath.Join(exportDir, exportName), data, 0600)
require.NoError(t, err)
n, _, err := c.DownloadExport(exportName, &buf, 0)
require.NoError(t, err)
require.Equal(t, len(data), int(n))
require.Equal(t, data, buf.Bytes())
}, "full download")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
err := os.Mkdir(exportDir, 0700)
require.NoError(t, err)
defer os.RemoveAll(exportDir)
data := randomBytes(t, 1024*1024)
var buf bytes.Buffer
exportName := "export.zip"
err = os.WriteFile(filepath.Join(exportDir, exportName), data, 0600)
require.NoError(t, err)
offset := 1024 * 512
n, _, err := c.DownloadExport(exportName, &buf, int64(offset))
require.NoError(t, err)
require.Equal(t, len(data)-offset, int(n))
require.Equal(t, data[offset:], buf.Bytes())
}, "download with offset")
}
func BenchmarkDownloadExport(b *testing.B) {
th := Setup(b)
defer th.TearDown()
dataDir, found := fileutils.FindDir("data")
require.True(b, found)
exportDir := filepath.Join(dataDir, *th.App.Config().ExportSettings.Directory)
err := os.Mkdir(exportDir, 0700)
require.NoError(b, err)
defer os.RemoveAll(exportDir)
exportName := "export.zip"
f, err := os.Create(filepath.Join(exportDir, exportName))
require.NoError(b, err)
f.Close()
err = os.Truncate(filepath.Join(exportDir, exportName), 1024*1024*1024)
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
outFilePath := filepath.Join(dataDir, fmt.Sprintf("export%d.zip", i))
outFile, _ := os.Create(outFilePath)
th.SystemAdminClient.DownloadExport(exportName, outFile, 0)
outFile.Close()
os.Remove(outFilePath)
}
}

758
server/channels/api4/file.go Обычный файл
Просмотреть файл

@@ -0,0 +1,758 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"crypto/subtle"
"encoding/json"
"io"
"mime"
"mime/multipart"
"net/http"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
const (
FileTeamId = "noteam"
PreviewImageType = "image/jpeg"
ThumbnailImageType = "image/jpeg"
)
const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
func (api *API) InitFile() {
api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST")
api.BaseRoutes.Files.Handle("/search", api.APISessionRequired(searchFilesForUser)).Methods("POST")
api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET")
api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET")
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods("GET")
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods("GET")
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods("POST")
api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods("GET")
}
func parseMultipartRequestHeader(req *http.Request) (boundary string, err error) {
v := req.Header.Get("Content-Type")
if v == "" {
return "", http.ErrNotMultipart
}
d, params, err := mime.ParseMediaType(v)
if err != nil || d != "multipart/form-data" {
return "", http.ErrNotMultipart
}
boundary, ok := params["boundary"]
if !ok {
return "", http.ErrMissingBoundary
}
return boundary, nil
}
func multipartReader(req *http.Request, stream io.Reader) (*multipart.Reader, error) {
boundary, err := parseMultipartRequestHeader(req)
if err != nil {
return nil, err
}
if stream != nil {
return multipart.NewReader(stream, boundary), nil
}
return multipart.NewReader(req.Body, boundary), nil
}
func uploadFileStream(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadFileStream",
"api.file.attachments.disabled.app_error",
nil, "", http.StatusForbidden)
return
}
// Parse the post as a regular form (in practice, use the URL values
// since we never expect a real application/x-www-form-urlencoded
// form).
if r.Form == nil {
err := r.ParseForm()
if err != nil {
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return
}
}
if r.ContentLength == 0 {
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, "Content-Length should not be 0", http.StatusBadRequest)
return
}
timestamp := time.Now()
var fileUploadResponse *model.FileUploadResponse
_, err := parseMultipartRequestHeader(r)
switch err {
case nil:
fileUploadResponse = uploadFileMultipart(c, r, nil, timestamp)
case http.ErrNotMultipart:
fileUploadResponse = uploadFileSimple(c, r, timestamp)
default:
c.Err = model.NewAppError("uploadFileStream",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
}
if c.Err != nil {
return
}
// Write the response values to the output upon return
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(fileUploadResponse); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
// uploadFileSimple uploads a file from a simple POST with the file in the request body
func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.FileUploadResponse {
// Simple POST with the file in the body and all metadata in the args.
c.RequireChannelId()
c.RequireFilename()
if c.Err != nil {
return nil
}
auditRec := c.MakeAuditRecord("uploadFileSimple", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
clientId := r.Form.Get("client_id")
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, c.Params.Filename, r.Body,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(r.ContentLength),
app.UploadFileSetClientId(clientId))
if appErr != nil {
c.Err = appErr
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
fileUploadResponse := &model.FileUploadResponse{
FileInfos: []*model.FileInfo{info},
}
if clientId != "" {
fileUploadResponse.ClientIds = []string{clientId}
}
auditRec.Success()
return fileUploadResponse
}
// uploadFileMultipart parses and uploads file(s) from a mime/multipart
// request. It pre-buffers up to the first part which is either the (a)
// `channel_id` value, or (b) a file. Then in case of (a) it re-processes the
// entire message recursively calling itself in stream mode. In case of (b) it
// calls to uploadFileMultipartLegacy for legacy support
func uploadFileMultipart(c *Context, r *http.Request, asStream io.Reader, timestamp time.Time) *model.FileUploadResponse {
expectClientIds := true
var clientIds []string
resp := model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
}
var buf *bytes.Buffer
var mr *multipart.Reader
var err error
if asStream == nil {
// We need to buffer until we get the channel_id, or the first file.
buf = &bytes.Buffer{}
mr, err = multipartReader(r, io.TeeReader(r.Body, buf))
} else {
mr, err = multipartReader(r, asStream)
}
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
nFiles := 0
NextPart:
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
// Parse any form fields in the multipart.
formname := part.FormName()
if formname == "" {
continue
}
filename := part.FileName()
if filename == "" {
var b bytes.Buffer
_, err = io.CopyN(&b, part, maxMultipartFormDataBytes)
if err != nil && err != io.EOF {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_form_value.app_error",
map[string]any{"Formname": formname},
err.Error(), http.StatusBadRequest)
return nil
}
v := b.String()
switch formname {
case "channel_id":
if c.Params.ChannelId != "" && c.Params.ChannelId != v {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.multiple_channel_ids.app_error",
nil, "", http.StatusBadRequest)
return nil
}
if v != "" {
c.Params.ChannelId = v
}
// Got channel_id, re-process the entire post
// in the streaming mode.
if asStream == nil {
return uploadFileMultipart(c, r, io.MultiReader(buf, r.Body), timestamp)
}
case "client_ids":
if !expectClientIds {
c.SetInvalidParam("client_ids")
return nil
}
clientIds = append(clientIds, v)
default:
c.SetInvalidParam(formname)
return nil
}
continue NextPart
}
// A file part.
if c.Params.ChannelId == "" && asStream == nil {
// Got file before channel_id, fall back to legacy buffered mode
mr, err = multipartReader(r, io.MultiReader(buf, r.Body))
if err != nil {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
return uploadFileMultipartLegacy(c, mr, timestamp)
}
c.RequireChannelId()
if c.Err != nil {
return nil
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
// If there's no clientIds when the first file comes, expect
// none later.
if nFiles == 0 && len(clientIds) == 0 {
expectClientIds = false
}
// Must have a exactly one client ID for each file.
clientId := ""
if expectClientIds {
if nFiles >= len(clientIds) {
c.SetInvalidParam("client_ids")
return nil
}
clientId = clientIds[nFiles]
}
auditRec := c.MakeAuditRecord("uploadFileMultipart", audit.Fail)
audit.AddEventParameter(auditRec, "channel_id", c.Params.ChannelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, filename, part,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
if appErr != nil {
c.Err = appErr
c.LogAuditRec(auditRec)
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
auditRec.Success()
c.LogAuditRec(auditRec)
// add to the response
resp.FileInfos = append(resp.FileInfos, info)
if expectClientIds {
resp.ClientIds = append(resp.ClientIds, clientId)
}
nFiles++
}
// Verify that the number of ClientIds matched the number of files.
if expectClientIds && len(clientIds) != nFiles {
c.Err = model.NewAppError("uploadFileMultipart",
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
map[string]any{"NumClientIds": len(clientIds), "NumFiles": nFiles},
"", http.StatusBadRequest)
return nil
}
return &resp
}
// uploadFileMultipartLegacy reads, buffers, and then uploads the message,
// borrowing from http.ParseMultipartForm. If successful it returns a
// *model.FileUploadResponse filled in with the individual model.FileInfo's.
func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
timestamp time.Time) *model.FileUploadResponse {
// Parse the entire form.
form, err := mr.ReadForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = model.NewAppError("uploadFileMultipartLegacy",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusInternalServerError)
return nil
}
// get and validate the channel Id, permission to upload there.
if len(form.Value["channel_id"]) == 0 {
c.SetInvalidParam("channel_id")
return nil
}
channelId := form.Value["channel_id"][0]
c.Params.ChannelId = channelId
c.RequireChannelId()
if c.Err != nil {
return nil
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionUploadFile) {
c.SetPermissionError(model.PermissionUploadFile)
return nil
}
// Check that we have either no client IDs, or one per file.
clientIds := form.Value["client_ids"]
fileHeaders := form.File["files"]
if len(clientIds) != 0 && len(clientIds) != len(fileHeaders) {
c.Err = model.NewAppError("uploadFilesMultipartBuffered",
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
map[string]any{"NumClientIds": len(clientIds), "NumFiles": len(fileHeaders)},
"", http.StatusBadRequest)
return nil
}
resp := model.FileUploadResponse{
FileInfos: []*model.FileInfo{},
ClientIds: []string{},
}
for i, fileHeader := range fileHeaders {
f, err := fileHeader.Open()
if err != nil {
c.Err = model.NewAppError("uploadFileMultipartLegacy",
"api.file.upload_file.read_request.app_error",
nil, err.Error(), http.StatusBadRequest)
return nil
}
clientId := ""
if len(clientIds) > 0 {
clientId = clientIds[i]
}
auditRec := c.MakeAuditRecord("uploadFileMultipartLegacy", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", channelId)
audit.AddEventParameter(auditRec, "client_id", clientId)
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, fileHeader.Filename, f,
app.UploadFileSetTeamId(FileTeamId),
app.UploadFileSetUserId(c.AppContext.Session().UserId),
app.UploadFileSetTimestamp(timestamp),
app.UploadFileSetContentLength(-1),
app.UploadFileSetClientId(clientId))
f.Close()
if appErr != nil {
c.Err = appErr
c.LogAuditRec(auditRec)
return nil
}
audit.AddEventParameterAuditable(auditRec, "file", info)
auditRec.Success()
c.LogAuditRec(auditRec)
resp.FileInfos = append(resp.FileInfos, info)
if clientId != "" {
resp.ClientIds = append(resp.ClientIds, clientId)
}
}
return &resp
}
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
auditRec := c.MakeAuditRecord("getFile", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "force_download", forceDownload)
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
audit.AddEventParameterAuditable(auditRec, "file", info)
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
fileReader, err := c.App.FileReader(info.Path)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
auditRec.Success()
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.ThumbnailPath == "" {
c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
fileReader, err := c.App.FileReader(info.ThumbnailPath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, ThumbnailImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
if !*c.App.Config().FileSettings.EnablePublicLink {
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
return
}
auditRec := c.MakeAuditRecord("getFileLink", audit.Fail)
defer c.LogAuditRec(auditRec)
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
audit.AddEventParameterAuditable(auditRec, "file", info)
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.PostId == "" {
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
resp := make(map[string]string)
link := c.App.GeneratePublicLink(c.GetSiteURLHeader(), info)
resp["link"] = link
auditRec.Success()
w.Write([]byte(model.MapToJSON(resp)))
}
func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if info.PreviewPath == "" {
c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
return
}
fileReader, err := c.App.FileReader(info.PreviewPath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, PreviewImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
}
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
if info.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), info.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
w.Header().Set("Cache-Control", "max-age=2592000, private")
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireFileId()
if c.Err != nil {
return
}
if !*c.App.Config().FileSettings.EnablePublicLink {
c.Err = model.NewAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
return
}
info, err := c.App.GetFileInfo(c.Params.FileId)
if err != nil {
c.Err = err
setInaccessibleFileHeader(w, err)
return
}
hash := r.URL.Query().Get("h")
if hash == "" {
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
return
}
if subtle.ConstantTimeCompare([]byte(hash), []byte(app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt))) != 1 {
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
return
}
fileReader, err := c.App.FileReader(info.Path)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, false, w, r)
}
func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
searchFiles(c, w, r, c.Params.TeamId)
}
func searchFilesForUser(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Config().FeatureFlags.CommandPalette {
searchFiles(c, w, r, "")
}
}
func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) {
var params model.SearchParameter
jsonErr := json.NewDecoder(r.Body).Decode(&params)
if jsonErr != nil {
c.Err = model.NewAppError("searchFiles", "api.post.search_files.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if params.Terms == nil || *params.Terms == "" {
c.SetInvalidParam("terms")
return
}
terms := *params.Terms
timeZoneOffset := 0
if params.TimeZoneOffset != nil {
timeZoneOffset = *params.TimeZoneOffset
}
isOrSearch := false
if params.IsOrSearch != nil {
isOrSearch = *params.IsOrSearch
}
page := 0
if params.Page != nil {
page = *params.Page
}
perPage := 60
if params.PerPage != nil {
perPage = *params.PerPage
}
includeDeletedChannels := false
if params.IncludeDeletedChannels != nil {
includeDeletedChannels = *params.IncludeDeletedChannels
}
modifier := ""
if params.Modifier != nil {
modifier = *params.Modifier
}
if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages {
c.SetInvalidParam("modifier")
return
}
startTime := time.Now()
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier)
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
metrics := c.App.Metrics()
if metrics != nil {
metrics.IncrementFilesSearchCounter()
metrics.ObserveFilesSearchDuration(elapsedTime)
}
if err != nil {
c.Err = err
return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(results); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setInaccessibleFileHeader(w http.ResponseWriter, appErr *model.AppError) {
// File is inaccessible due to cloud plan's limit.
if appErr.Id == "app.file.cloud.get.app_error" {
w.Header().Set(model.HeaderFirstInaccessibleFileTime, "1")
}
}

1267
server/channels/api4/file_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

193
server/channels/api4/graphql.go Обычный файл
Просмотреть файл

@@ -0,0 +1,193 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"github.com/graph-gophers/dataloader/v6"
graphql "github.com/graph-gophers/graphql-go"
gqlerrors "github.com/graph-gophers/graphql-go/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type graphQLInput struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]any `json:"variables"`
}
// Unique type to hold our context.
type ctxKey int
const (
webCtx ctxKey = 0
rolesLoaderCtx ctxKey = 1
channelsLoaderCtx ctxKey = 2
teamsLoaderCtx ctxKey = 3
usersLoaderCtx ctxKey = 4
)
const loaderBatchCapacity = web.PerPageMaximum
//go:embed schema.graphqls
var schemaRaw string
func (api *API) InitGraphQL() error {
// Guard with a feature flag.
if !api.srv.Config().FeatureFlags.GraphQL {
return nil
}
var err error
opts := []graphql.SchemaOpt{
graphql.UseFieldResolvers(),
graphql.Logger(mlog.NewGraphQLLogger(api.srv.Log())),
graphql.MaxParallelism(loaderBatchCapacity), // This is dangerous if the query
// uses any non-dataloader backed object. So we need to be a bit careful here.
}
if isProd() {
opts = append(opts,
// MaxDepth cannot be moved as a general param
// because otherwise introspection also doesn't work
// with just a depth of 4.
graphql.MaxDepth(4),
graphql.DisableIntrospection(),
)
}
api.schema, err = graphql.ParseSchema(schemaRaw, &resolver{}, opts...)
if err != nil {
return err
}
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APIHandlerTrustRequester(graphiQL)).Methods("GET")
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APISessionRequired(api.graphQL)).Methods("POST")
return nil
}
func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
var response *graphql.Response
defer func() {
if response != nil {
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
}()
// Limit bodies to 100KiB.
// We need to enforce a lower limit than the file upload size,
// to prevent the library doing unnecessary parsing.
r.Body = http.MaxBytesReader(w, r.Body, 102400)
var params graphQLInput
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
err2 := gqlerrors.Errorf("invalid request body: %v", err)
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
return
}
if isProd() && params.OperationName == "" {
err2 := gqlerrors.Errorf("operation name not passed")
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
return
}
c.GraphQLOperationName = params.OperationName
// Populate the context with required info.
reqCtx := r.Context()
reqCtx = context.WithValue(reqCtx, webCtx, c)
rolesLoader := dataloader.NewBatchedLoader(graphQLRolesLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, rolesLoaderCtx, rolesLoader)
channelsLoader := dataloader.NewBatchedLoader(graphQLChannelsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, channelsLoaderCtx, channelsLoader)
teamsLoader := dataloader.NewBatchedLoader(graphQLTeamsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, teamsLoaderCtx, teamsLoader)
usersLoader := dataloader.NewBatchedLoader(graphQLUsersLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, usersLoaderCtx, usersLoader)
response = api.schema.Exec(reqCtx,
params.Query,
params.OperationName,
params.Variables)
if len(response.Errors) > 0 {
logFunc := mlog.Error
for _, gqlErr := range response.Errors {
if gqlErr.Err != nil {
if appErr, ok := gqlErr.Err.(*model.AppError); ok && appErr.StatusCode < http.StatusInternalServerError {
logFunc = mlog.Debug
break
}
}
}
logFunc("Error executing request", mlog.String("operation", params.OperationName),
mlog.Array("errors", response.Errors))
}
}
func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write(graphiqlPage)
}
var graphiqlPage = []byte(`
<!DOCTYPE html>
<html>
<head>
<title>GraphiQL editor | Mattermost</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" integrity="sha256-gSgd+on4bTXigueyd/NSRNAy4cBY42RAVNaXnQDjOW8=" crossorigin="anonymous"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha256-OI3N9zCKabDov2rZFzl8lJUXCcP7EmsGcGoP6DMXQCo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js" integrity="sha256-aB35laj7IZhLTx58xw/Gm1EKOoJJKZt6RY+bH1ReHxs=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js" integrity="sha256-wouRkivKKXA3y6AuyFwcDcF50alCNV8LbghfYCH6Z98=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js" integrity="sha256-9hrJxD4IQsWHdNpzLkJKYGiY/SEZFJJSUqyeZPNKd8g=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js" integrity="sha256-oeWyQyKKUurcnbFRsfeSgrdOpXXiRYopnPjTVZ+6UmI=" crossorigin="anonymous"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/api/v5/graphql", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)
// isProd is a helper function to apply prod-specific graphQL validations.
func isProd() bool {
return model.BuildNumber != "dev"
}

87
server/channels/api4/graphql_client.go Обычный файл
Просмотреть файл

@@ -0,0 +1,87 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
// graphQLClient is an internal test client to run the tests.
// When the API matures, we will expose it to the model package.
type graphQLClient struct {
URL string // The location of the server, for example "http://localhost:8065"
APIURL string // The api location of the server, for example "http://localhost:8065/api/v4"
httpClient *http.Client // The http client
authToken string
authType string
httpHeader map[string]string // Headers to be copied over for each request
}
func newGraphQLClient(url string) *graphQLClient {
url = strings.TrimRight(url, "/")
return &graphQLClient{url, url + model.APIURLSuffix, &http.Client{}, "", "", map[string]string{}}
}
func (c *graphQLClient) login(loginId string, password string) (*model.User, *model.Response, error) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
r, err := c.doAPIRequest(http.MethodPost, c.APIURL+"/users/login", strings.NewReader(model.MapToJSON(m)), map[string]string{model.HeaderEtagClient: ""})
if err != nil {
return nil, model.BuildResponse(r), err
}
defer closeBody(r)
c.authToken = r.Header.Get(model.HeaderToken)
c.authType = model.HeaderBearer
var user model.User
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
return nil, nil, model.NewAppError("login", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &user, model.BuildResponse(r), nil
}
func (c *graphQLClient) doAPIRequest(method, url string, data io.Reader, headers map[string]string) (*http.Response, error) {
rq, err := c.prepareRequest(method, url, data, headers)
if err != nil {
return nil, err
}
rp, err := c.httpClient.Do(rq)
if err != nil {
return rp, err
}
return rp, nil
}
func (c *graphQLClient) prepareRequest(method, url string, data io.Reader, headers map[string]string) (*http.Request, error) {
rq, err := http.NewRequest(method, url, data)
if err != nil {
return nil, err
}
for k, v := range headers {
rq.Header.Set(k, v)
}
if c.authToken != "" {
rq.Header.Set(model.HeaderAuth, c.authType+" "+c.authToken)
}
if c.httpHeader != nil && len(c.httpHeader) > 0 {
for k, v := range c.httpHeader {
rq.Header.Set(k, v)
}
}
return rq, nil
}

34
server/channels/api4/graphql_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestGraphQLPayload(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t).InitBasic()
defer th.TearDown()
largeString := strings.Repeat("hello", 204800)
input := graphQLInput{
OperationName: "config",
Query: largeString,
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 1)
// The actual error isn't exposed. We compare the string
// to not confuse with other errors.
require.Contains(t, resp.Errors[0].Message, "request body too large")
}

1351
server/channels/api4/group.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

41
server/channels/api4/group_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
)
func (api *API) InitGroupLocal() {
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannelLocal)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeamLocal)).Methods("GET")
}
func getGroupsByChannelLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
w.Write(b)
}
func getGroupsByTeamLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
w.Write(b)
}

1688
server/channels/api4/group_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

226
server/channels/api4/handlers.go Обычный файл
Просмотреть файл

@@ -0,0 +1,226 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/gziphandler"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
type Context = web.Context
type handlerFunc func(*Context, http.ResponseWriter, *http.Request)
// APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
// granted.
func (api *API) APIHandler(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
// be granted.
func (api *API) APISessionRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
RequireCloudKey: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints.
func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
RequireCloudKey: false,
RequireRemoteClusterToken: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session but when accessed,
// if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
// authentication must be waived.
func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
// websocket.
func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: true,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and
// are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads.
func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: true,
RequireMfa: true,
IsStatic: false,
IsLocal: false,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load,
// responding with HTTP 503 (Service Unavailable).
func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: false,
DisableWhenBusy: true,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
// APILocal provides a handler for API endpoints to be used in local
// mode, this is, through a UNIX socket and without an authenticated
// session, but with one that has no user set and no permission
// restrictions
func (api *API) APILocal(h handlerFunc) http.Handler {
handler := &web.Handler{
Srv: api.srv,
HandleFunc: h,
HandlerName: web.GetHandlerName(h),
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
IsLocal: true,
}
if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
return gziphandler.GzipHandler(handler)
}
return handler
}
func requireLicense(c *Context) *model.AppError {
if c.App.Channels().License() == nil {
err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
func minimumProfessionalLicense(c *Context) *model.AppError {
lic := c.App.Srv().License()
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
return err
}
return nil
}
func rejectGuests(c *Context) *model.AppError {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
return err
}
return nil
}

95
server/channels/api4/handlers_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func handlerForGzip(c *Context, w http.ResponseWriter, r *http.Request) {
// gziphandler default requires body size greater than 1400 bytes
var body [1400]byte
w.Write(body[:])
}
func testAPIHandlerGzipMode(t *testing.T, name string, h http.Handler, token string) {
t.Run("Handler: "+name+" No Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req.Header.Set(model.HeaderAuth, "Bearer "+token)
h.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "", resp.Header().Get("Content-Encoding"))
})
t.Run("Handler: "+name+" With Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req.Header.Set("Accept-Encoding", "gzip")
req.Header.Set(model.HeaderAuth, "Bearer "+token)
h.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "gzip", resp.Header().Get("Content-Encoding"))
})
}
func testAPIHandlerNoGzipMode(t *testing.T, name string, h http.Handler, token string) {
t.Run("Handler: "+name+" No Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req.Header.Set(model.HeaderAuth, "Bearer "+token)
h.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "", resp.Header().Get("Content-Encoding"))
})
t.Run("Handler: "+name+" With Accept-Encoding", func(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v4/test", nil)
req.Header.Set("Accept-Encoding", "gzip")
req.Header.Set(model.HeaderAuth, "Bearer "+token)
h.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "", resp.Header().Get("Content-Encoding"))
})
}
func TestAPIHandlersWithGzip(t *testing.T) {
th := Setup(t)
defer th.TearDown()
api, err := Init(th.Server)
require.NoError(t, err)
session, _ := th.App.GetSession(th.Client.AuthToken)
t.Run("with WebserverMode == \"gzip\"", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.WebserverMode = "gzip" })
testAPIHandlerGzipMode(t, "ApiHandler", api.APIHandler(handlerForGzip), "")
testAPIHandlerGzipMode(t, "ApiSessionRequired", api.APISessionRequired(handlerForGzip), session.Token)
testAPIHandlerGzipMode(t, "ApiSessionRequiredMfa", api.APISessionRequiredMfa(handlerForGzip), session.Token)
testAPIHandlerGzipMode(t, "ApiHandlerTrustRequester", api.APIHandlerTrustRequester(handlerForGzip), "")
testAPIHandlerGzipMode(t, "ApiSessionRequiredTrustRequester", api.APISessionRequiredTrustRequester(handlerForGzip), session.Token)
})
t.Run("with WebserverMode == \"nogzip\"", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.WebserverMode = "nogzip" })
testAPIHandlerNoGzipMode(t, "ApiHandler", api.APIHandler(handlerForGzip), "")
testAPIHandlerNoGzipMode(t, "ApiSessionRequired", api.APISessionRequired(handlerForGzip), session.Token)
testAPIHandlerNoGzipMode(t, "ApiSessionRequiredMfa", api.APISessionRequiredMfa(handlerForGzip), session.Token)
testAPIHandlerNoGzipMode(t, "ApiHandlerTrustRequester", api.APIHandlerTrustRequester(handlerForGzip), "")
testAPIHandlerNoGzipMode(t, "ApiSessionRequiredTrustRequester", api.APISessionRequiredTrustRequester(handlerForGzip), session.Token)
})
}

25
server/channels/api4/helpers.go Обычный файл
Просмотреть файл

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/url"
"strconv"
"github.com/pkg/errors"
)
func parseInt(u *url.URL, name string, defaultValue int) (int, error) {
valueStr := u.Query().Get(name)
if valueStr == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return 0, errors.Wrapf(err, "failed to parse %s as integer", name)
}
return value, nil
}

290
server/channels/api4/hosted_customer.go Обычный файл
Просмотреть файл

@@ -0,0 +1,290 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
// Endpoints for cloud installations should not go in this file.
func (api *API) InitHostedCustomer() {
// POST /api/v4/hosted_customer/available
api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods("GET")
// POST /api/v4/hosted_customer/bootstrap
api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST")
// POST /api/v4/hosted_customer/customer
api.BaseRoutes.HostedCustomer.Handle("/customer", api.APISessionRequired(selfHostedCustomer)).Methods("POST")
// POST /api/v4/hosted_customer/confirm
api.BaseRoutes.HostedCustomer.Handle("/confirm", api.APISessionRequired(selfHostedConfirm)).Methods("POST")
// GET /api/v4/hosted_customer/invoices
api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET")
// GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf
api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET")
}
func ensureSelfHostedAdmin(c *Context, where string) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError(where, "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
return
}
license := c.App.Channels().License()
if license.IsCloud() {
c.Err = model.NewAppError(where, "api.cloud.license_error", nil, "Cloud installations do not use this endpoint", http.StatusBadRequest)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
return
}
}
func checkSelfHostedPurchaseEnabled(c *Context) bool {
config := c.App.Config()
if config == nil {
return false
}
enabled := config.ServiceSettings.SelfHostedPurchase
return enabled != nil && *enabled
}
func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedBootstrap"
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
reset := r.URL.Query().Get("reset") == "true"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email, Reset: reset})
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
json, err := json.Marshal(signupProgress)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}
func selfHostedCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedCustomer"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var form *model.SelfHostedCustomerForm
if err = json.Unmarshal(bodyBytes, &form); err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
customerResponse, err := c.App.Cloud().CreateCustomerSelfHostedSignup(*form, user.Email)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(customerResponse)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedConfirm(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedConfirm"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var confirm model.SelfHostedConfirmPaymentMethodRequest
err = json.Unmarshal(bodyBytes, &confirm)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = userErr
return
}
confirmResponse, err := c.App.Cloud().ConfirmSelfHostedSignup(confirm, user.Email)
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
if err.Error() == fmt.Sprintf("%d", http.StatusUnprocessableEntity) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
return
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
license, err := c.App.Srv().Platform().SaveLicense([]byte(confirmResponse.License))
// dealing with an AppError
if !(reflect.ValueOf(err).Kind() == reflect.Ptr && reflect.ValueOf(err).IsNil()) {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
clientResponse, err := json.Marshal(model.SelfHostedSignupConfirmClientResponse{
License: utils.GetClientLicense(license),
Progress: confirmResponse.Progress,
})
if err != nil {
if confirmResponse != nil {
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
}
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
go func() {
err := c.App.Cloud().ConfirmSelfHostedSignupLicenseApplication()
if err != nil {
c.Logger.Warn("Unable to confirm license application", mlog.Err(err))
}
}()
_, _ = w.Write(clientResponse)
}
func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.handleSignupAvailable"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
if !checkSelfHostedPurchaseEnabled(c) {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.Cloud().SelfHostedSignupAvailable(); err != nil {
if err.Error() == "upstream_off" {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusServiceUnavailable)
} else {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented)
}
return
}
systemValue, err := c.App.Srv().Store().System().GetByName(model.SystemHostedPurchaseNeedsScreening)
if err == nil && systemValue != nil {
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusTooEarly)
return
}
ReturnStatusOK(w)
}
func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoices"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
invoices, err := c.App.Cloud().GetSelfHostedInvoices()
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
json, err := json.Marshal(invoices)
if err != nil {
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
const where = "Api4.selfHostedInvoicePDF"
ensureSelfHostedAdmin(c, where)
if c.Err != nil {
return
}
pdfData, filename, appErr := c.App.Cloud().GetSelfHostedInvoicePDF(c.Params.InvoiceId)
if appErr != nil {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
return
}
web.WriteFileResponse(
filename,
"application/pdf",
int64(binary.Size(pdfData)),
time.Now(),
*c.App.Config().ServiceSettings.WebserverMode,
bytes.NewReader(pdfData),
false,
w,
r,
)
}

144
server/channels/api4/hosted_customer_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,144 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"os"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces/mocks"
)
var valFalse = false
var valTrue = true
func TestSelfHostedBootstrap(t *testing.T) {
t.Run("feature flag off returns not implemented", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloud := mocks.CloudInterface{}
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "false")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valFalse })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusNotImplemented, r.StatusCode)
require.Error(t, err)
})
t.Run("cloud instances not allowed to bootstrap self-hosted signup", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloud := mocks.CloudInterface{}
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusBadRequest, r.StatusCode)
require.Error(t, err)
})
t.Run("non-admins not allowed to bootstrap self-hosted signup", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloud := mocks.CloudInterface{}
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusForbidden, r.StatusCode)
require.Error(t, err)
})
t.Run("self-hosted admins can bootstrap self-hosted signup", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue })
th.App.ReloadConfig()
cloud := mocks.CloudInterface{}
cloud.Mock.On("BootstrapSelfHostedSignup", mock.Anything).Return(&model.BootstrapSelfHostedSignupResponse{Progress: "START"}, nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
response, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, err)
require.Equal(t, "START", response.Progress)
})
t.Run("team edition returns bad request instead of panicking", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = nil
th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password)
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue })
th.App.ReloadConfig()
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
require.Equal(t, http.StatusBadRequest, r.StatusCode)
require.Error(t, err)
})
}

46
server/channels/api4/image.go Обычный файл
Просмотреть файл

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"net/url"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitImage() {
api.BaseRoutes.Image.Handle("", api.APISessionRequiredTrustRequester(getImage)).Methods("GET")
}
func getImage(c *Context, w http.ResponseWriter, r *http.Request) {
actualURL := r.URL.Query().Get("url")
parsedURL, err := url.Parse(actualURL)
if err != nil {
c.Err = model.NewAppError("getImage", "api.image.get.app_error", nil, err.Error(), http.StatusBadRequest)
return
} else if parsedURL.Opaque != "" {
c.Err = model.NewAppError("getImage", "api.image.get.app_error", nil, "", http.StatusBadRequest)
return
}
siteURL, err := url.Parse(*c.App.Config().ServiceSettings.SiteURL)
if err != nil {
c.Err = model.NewAppError("getImage", "model.config.is_valid.site_url.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if parsedURL.Scheme == "" {
parsedURL.Scheme = siteURL.Scheme
}
if parsedURL.Host == "" {
parsedURL.Host = siteURL.Host
}
// in case image proxy is enabled and we are fetching a remote image (NOT static or served by plugins), pass request to proxy
if *c.App.Config().ImageProxySettings.Enable && parsedURL.Host != siteURL.Host {
c.App.ImageProxy().GetImage(w, r, parsedURL.String())
} else {
http.Redirect(w, r, parsedURL.String(), http.StatusFound)
}
}

126
server/channels/api4/image_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,126 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGetImage(t *testing.T) {
th := Setup(t)
defer th.TearDown()
// Prevent the test client from following a redirect
th.Client.HTTPClient.CheckRedirect = func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}
t.Run("proxy disabled", func(t *testing.T) {
imageURL := "http://foo.bar/baz.gif"
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ImageProxySettings.Enable = model.NewBool(false)
})
r, err := http.NewRequest("GET", th.Client.APIURL+"/image?url="+url.QueryEscape(imageURL), nil)
require.NoError(t, err)
r.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
resp, err := th.Client.HTTPClient.Do(r)
require.NoError(t, err)
assert.Equal(t, http.StatusFound, resp.StatusCode)
assert.Equal(t, imageURL, resp.Header.Get("Location"))
})
t.Run("atmos/camo", func(t *testing.T) {
imageURL := "http://foo.bar/baz.gif"
proxiedURL := "https://proxy.foo.bar/004afe2ef382eb5f30c4490f793f8a8c5b33d8a2/687474703a2f2f666f6f2e6261722f62617a2e676966"
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ImageProxySettings.Enable = model.NewBool(true)
cfg.ImageProxySettings.ImageProxyType = model.NewString("atmos/camo")
cfg.ImageProxySettings.RemoteImageProxyOptions = model.NewString("foo")
cfg.ImageProxySettings.RemoteImageProxyURL = model.NewString("https://proxy.foo.bar")
})
r, err := http.NewRequest("GET", th.Client.APIURL+"/image?url="+url.QueryEscape(imageURL), nil)
require.NoError(t, err)
r.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
resp, err := th.Client.HTTPClient.Do(r)
require.NoError(t, err)
assert.Equal(t, http.StatusFound, resp.StatusCode)
assert.Equal(t, proxiedURL, resp.Header.Get("Location"))
})
t.Run("local", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ImageProxySettings.Enable = model.NewBool(true)
cfg.ImageProxySettings.ImageProxyType = model.NewString("local")
// Allow requests to the "remote" image
cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewString("127.0.0.1")
})
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.Write([]byte("success"))
})
imageServer := httptest.NewServer(handler)
defer imageServer.Close()
r, err := http.NewRequest("GET", th.Client.APIURL+"/image?url="+url.QueryEscape(imageServer.URL+"/image.png"), nil)
require.NoError(t, err)
r.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
resp, err := th.Client.HTTPClient.Do(r)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "success", string(respBody))
// local images should not be proxied, but forwarded
r, err = http.NewRequest("GET", th.Client.APIURL+"/image?url=/plugins/test/image.png", nil)
require.NoError(t, err)
r.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
resp, err = th.Client.HTTPClient.Do(r)
require.NoError(t, err)
assert.Equal(t, http.StatusFound, resp.StatusCode)
// protocol relative URLs should be handled by proxy
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.SiteURL = model.NewString("http://foo.com")
})
r, err = http.NewRequest("GET", th.Client.APIURL+"/image?url="+strings.TrimPrefix(imageServer.URL, "http:")+"/image.png", nil)
require.NoError(t, err)
r.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
resp, err = th.Client.HTTPClient.Do(r)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
// opaque URLs are not supported, should return an error
r, err = http.NewRequest("GET", th.Client.APIURL+"/image?url=mailto:test@example.com", nil)
require.NoError(t, err)
r.Header.Set(model.HeaderAuth, th.Client.AuthType+" "+th.Client.AuthToken)
resp, err = th.Client.HTTPClient.Do(r)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}

33
server/channels/api4/import.go Обычный файл
Просмотреть файл

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitImport() {
api.BaseRoutes.Imports.Handle("", api.APISessionRequired(listImports)).Methods("GET")
}
func listImports(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
imports, appErr := c.App.ListImports()
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(imports); err != nil {
c.Logger.Warn("Error writing imports", mlog.Err(err))
}
}

8
server/channels/api4/import_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitImportLocal() {
api.BaseRoutes.Imports.Handle("", api.APILocal(listImports)).Methods("GET")
}

106
server/channels/api4/import_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,106 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
func TestListImports(t *testing.T) {
th := Setup(t)
defer th.TearDown()
testsDir, _ := fileutils.FindDir("tests")
require.NotEmpty(t, testsDir)
uploadNewImport := func(c *model.Client4, t *testing.T) string {
file, err := os.Open(testsDir + "/import_test.zip")
require.NoError(t, err)
info, err := file.Stat()
require.NoError(t, err)
us := &model.UploadSession{
Filename: info.Name(),
FileSize: info.Size(),
Type: model.UploadTypeImport,
}
if c == th.LocalClient {
us.UserId = model.UploadNoUserID
}
u, _, err := c.CreateUpload(us)
require.NoError(t, err)
require.NotNil(t, u)
finfo, _, err := c.UploadData(u.Id, file)
require.NoError(t, err)
require.NotNil(t, finfo)
return u.Id
}
t.Run("no permissions", func(t *testing.T) {
imports, _, err := th.Client.ListImports()
require.Error(t, err)
CheckErrorID(t, err, "api.context.permissions.app_error")
require.Nil(t, imports)
})
dataDir, found := fileutils.FindDir("data")
require.True(t, found)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
imports, _, err := c.ListImports()
require.NoError(t, err)
require.Empty(t, imports)
}, "no imports")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
id := uploadNewImport(c, t)
id2 := uploadNewImport(c, t)
importDir := filepath.Join(dataDir, "import")
f, err := os.Create(filepath.Join(importDir, "import.zip.tmp"))
require.NoError(t, err)
f.Close()
imports, _, err := c.ListImports()
require.NoError(t, err)
require.NotEmpty(t, imports)
require.Len(t, imports, 2)
require.Contains(t, imports, id+"_import_test.zip")
require.Contains(t, imports, id2+"_import_test.zip")
require.NoError(t, os.RemoveAll(importDir))
}, "expected imports")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ImportSettings.Directory = "import_new" })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ImportSettings.Directory = "import" })
importDir := filepath.Join(dataDir, "import_new")
imports, _, err := c.ListImports()
require.NoError(t, err)
require.Empty(t, imports)
id := uploadNewImport(c, t)
imports, _, err = c.ListImports()
require.NoError(t, err)
require.NotEmpty(t, imports)
require.Len(t, imports, 1)
require.Equal(t, id+"_import_test.zip", imports[0])
require.NoError(t, os.RemoveAll(importDir))
}, "change import directory")
}

637
server/channels/api4/insights.go Обычный файл
Просмотреть файл

@@ -0,0 +1,637 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitInsights() {
// Reactions
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET")
// Channels
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET")
// Threads
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(getTopThreadsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(getTopThreadsForUserSince)).Methods("GET")
// user DMs
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(getTopDMsForUserSince)).Methods("GET")
// Inactive channels
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForUserSince)).Methods("GET")
// New teammembers
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(getNewTeamMembersSince)).Methods("GET")
}
// Top Reactions
func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, nil, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForUserSince(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, &c.AppContext.Session().UserId, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Threads
func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top DMs
func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topDMs); err != nil {
c.Err = model.NewAppError("getTopDMsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// top inactive channels
func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view.
func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) {
if len(topChannelList.Items) == 0 {
return nil, nil
}
var postCountsByDay []*model.DurationPostCount
channelIDs := topChannelList.ChannelIDs()
var grouping model.PostCountGrouping
if timeRange == model.TimeRangeToday {
grouping = model.PostsByHour
} else {
grouping = model.PostsByDay
}
postCountsByDay, err := c.App.PostCountsByDuration(c.AppContext, channelIDs, startTime.UnixMilli(), userID, grouping, location)
if err != nil {
return nil, err
}
return model.ToDailyPostCountViewModel(postCountsByDay, startTime, model.TimeRangeToNumberDays(timeRange), channelIDs), nil
}
func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
ntms, count, err := c.App.GetNewTeamMembersSince(c.AppContext, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
ntms.TotalCount = count
if err := json.NewEncoder(w).Encode(ntms); err != nil {
c.Err = model.NewAppError("getNewTeamembersForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}

1243
server/channels/api4/insights_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

130
server/channels/api4/integration_action.go Обычный файл
Просмотреть файл

@@ -0,0 +1,130 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitAction() {
api.BaseRoutes.Post.Handle("/actions/{action_id:[A-Za-z0-9]+}", api.APISessionRequired(doPostAction)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/open", api.APIHandler(openDialog)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/actions/dialogs/submit", api.APISessionRequired(submitDialog)).Methods("POST")
}
func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
var actionRequest model.DoPostActionRequest
err := json.NewDecoder(r.Body).Decode(&actionRequest)
if err != nil {
c.Logger.Warn("Error decoding the action request", mlog.Err(err))
}
var cookie *model.PostActionCookie
if actionRequest.Cookie != "" {
cookie = &model.PostActionCookie{}
cookieStr := ""
cookieStr, err = model.DecryptPostActionCookie(actionRequest.Cookie, c.App.PostActionCookieSecret())
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
err = json.Unmarshal([]byte(cookieStr), &cookie)
if err != nil {
c.Err = model.NewAppError("DoPostAction", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), cookie.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
} else {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
var appErr *model.AppError
resp := &model.PostActionAPIResponse{Status: "OK"}
resp.TriggerId, appErr = c.App.DoPostActionWithCookie(c.AppContext, c.Params.PostId, c.Params.ActionId, c.AppContext.Session().UserId,
actionRequest.SelectedOption, cookie)
if appErr != nil {
c.Err = appErr
return
}
err = json.NewEncoder(w).Encode(resp)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func openDialog(c *Context, w http.ResponseWriter, r *http.Request) {
var dialog model.OpenDialogRequest
err := json.NewDecoder(r.Body).Decode(&dialog)
if err != nil {
c.SetInvalidParamWithErr("dialog", err)
return
}
if dialog.URL == "" {
c.SetInvalidParam("url")
return
}
if appErr := c.App.OpenInteractiveDialog(dialog); appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func submitDialog(c *Context, w http.ResponseWriter, r *http.Request) {
var submit model.SubmitDialogRequest
jsonErr := json.NewDecoder(r.Body).Decode(&submit)
if jsonErr != nil {
c.SetInvalidParamWithErr("dialog", jsonErr)
return
}
if submit.URL == "" {
c.SetInvalidParam("url")
return
}
submit.UserId = c.AppContext.Session().UserId
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), submit.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), submit.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
resp, err := c.App.SubmitInteractiveDialog(c.AppContext, submit)
if err != nil {
c.Err = err
return
}
b, _ := json.Marshal(resp)
w.Write(b)
}

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

@@ -0,0 +1,275 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
type testHandler struct {
t *testing.T
}
func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
bb, err := io.ReadAll(r.Body)
assert.NoError(th.t, err)
assert.NotEmpty(th.t, string(bb))
var poir model.PostActionIntegrationRequest
jsonErr := json.Unmarshal(bb, &poir)
assert.NoError(th.t, jsonErr)
assert.NotEmpty(th.t, poir.UserId)
assert.NotEmpty(th.t, poir.UserName)
assert.NotEmpty(th.t, poir.ChannelId)
assert.NotEmpty(th.t, poir.ChannelName)
assert.NotEmpty(th.t, poir.TeamId)
assert.NotEmpty(th.t, poir.TeamName)
assert.NotEmpty(th.t, poir.PostId)
assert.NotEmpty(th.t, poir.TriggerId)
assert.Equal(th.t, "button", poir.Type)
assert.Equal(th.t, "test-value", poir.Context["test-key"])
w.Write([]byte("{}"))
w.WriteHeader(200)
}
func TestPostActionCookies(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
})
handler := &testHandler{t}
server := httptest.NewServer(handler)
for name, test := range map[string]struct {
Action model.PostAction
ExpectedSuccess bool
ExpectedStatusCode int
}{
"32 character ID": {
Action: model.PostAction{
Id: model.NewId(),
Name: "Test-action",
Type: model.PostActionTypeButton,
Integration: &model.PostActionIntegration{
URL: server.URL,
Context: map[string]any{
"test-key": "test-value",
},
},
},
ExpectedSuccess: true,
ExpectedStatusCode: http.StatusOK,
},
"6 character ID": {
Action: model.PostAction{
Id: "someID",
Name: "Test-action",
Type: model.PostActionTypeButton,
Integration: &model.PostActionIntegration{
URL: server.URL,
Context: map[string]any{
"test-key": "test-value",
},
},
},
ExpectedSuccess: true,
ExpectedStatusCode: http.StatusOK,
},
"Empty ID": {
Action: model.PostAction{
Id: "",
Name: "Test-action",
Type: model.PostActionTypeButton,
Integration: &model.PostActionIntegration{
URL: server.URL,
Context: map[string]any{
"test-key": "test-value",
},
},
},
ExpectedSuccess: false,
ExpectedStatusCode: http.StatusNotFound,
},
} {
t.Run(name, func(t *testing.T) {
post := &model.Post{
Id: model.NewId(),
Type: model.PostTypeEphemeral,
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
Props: map[string]any{
"attachments": []*model.SlackAttachment{
{
Title: "some-title",
TitleLink: "https://some-url.com",
Text: "some-text",
ImageURL: "https://some-other-url.com",
Actions: []*model.PostAction{&test.Action},
},
},
},
}
assert.Equal(t, 32, len(th.App.PostActionCookieSecret()))
post = model.AddPostActionCookies(post, th.App.PostActionCookieSecret())
resp, err := client.DoPostActionWithCookie(post.Id, test.Action.Id, "", test.Action.Cookie)
require.NotNil(t, resp)
if test.ExpectedSuccess {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
assert.Equal(t, test.ExpectedStatusCode, resp.StatusCode)
assert.NotNil(t, resp.RequestId)
assert.NotNil(t, resp.ServerVersion)
})
}
}
func TestOpenDialog(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
})
_, triggerId, appErr := model.GenerateTriggerId(th.BasicUser.Id, th.App.AsymmetricSigningKey())
require.Nil(t, appErr)
request := model.OpenDialogRequest{
TriggerId: triggerId,
URL: "http://localhost:8065",
Dialog: model.Dialog{
CallbackId: "callbackid",
Title: "Some Title",
Elements: []model.DialogElement{
{
DisplayName: "Element Name",
Name: "element_name",
Type: "text",
Placeholder: "Enter a value",
},
},
SubmitLabel: "Submit",
NotifyOnCancel: false,
State: "somestate",
},
}
_, err := client.OpenInteractiveDialog(request)
require.NoError(t, err)
// Should fail on bad trigger ID
request.TriggerId = "junk"
resp, err := client.OpenInteractiveDialog(request)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// URL is required
request.TriggerId = triggerId
request.URL = ""
resp, err = client.OpenInteractiveDialog(request)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Should pass with markdown formatted introduction text
request.URL = "http://localhost:8065"
request.Dialog.IntroductionText = "**Some** _introduction text"
_, err = client.OpenInteractiveDialog(request)
require.NoError(t, err)
// Should pass with empty introduction text
request.Dialog.IntroductionText = ""
_, err = client.OpenInteractiveDialog(request)
require.NoError(t, err)
// Should pass with no elements
request.Dialog.Elements = nil
_, err = client.OpenInteractiveDialog(request)
require.NoError(t, err)
request.Dialog.Elements = []model.DialogElement{}
_, err = client.OpenInteractiveDialog(request)
require.NoError(t, err)
}
func TestSubmitDialog(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
})
submit := model.SubmitDialogRequest{
CallbackId: "callbackid",
State: "somestate",
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
TeamId: th.BasicTeam.Id,
Submission: map[string]any{"somename": "somevalue"},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request model.SubmitDialogRequest
err := json.NewDecoder(r.Body).Decode(&request)
require.NoError(t, err)
assert.Equal(t, request.URL, "")
assert.Equal(t, request.UserId, submit.UserId)
assert.Equal(t, request.ChannelId, submit.ChannelId)
assert.Equal(t, request.TeamId, submit.TeamId)
assert.Equal(t, request.CallbackId, submit.CallbackId)
assert.Equal(t, request.State, submit.State)
val, ok := request.Submission["somename"].(string)
require.True(t, ok)
assert.Equal(t, "somevalue", val)
}))
defer ts.Close()
submit.URL = ts.URL
submitResp, _, err := client.SubmitInteractiveDialog(submit)
require.NoError(t, err)
assert.NotNil(t, submitResp)
submit.URL = ""
submitResp, resp, err := client.SubmitInteractiveDialog(submit)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Nil(t, submitResp)
submit.URL = ts.URL
submit.ChannelId = model.NewId()
submitResp, resp, err = client.SubmitInteractiveDialog(submit)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, submitResp)
submit.URL = ts.URL
submit.ChannelId = th.BasicChannel.Id
submit.TeamId = model.NewId()
submitResp, resp, err = client.SubmitInteractiveDialog(submit)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
assert.Nil(t, submitResp)
}

250
server/channels/api4/job.go Обычный файл
Просмотреть файл

@@ -0,0 +1,250 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"path/filepath"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
func (api *API) InitJob() {
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(getJobs)).Methods("GET")
api.BaseRoutes.Jobs.Handle("", api.APISessionRequired(createJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}", api.APISessionRequired(getJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/download", api.APISessionRequiredTrustRequester(downloadJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/cancel", api.APISessionRequired(cancelJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/type/{job_type:[A-Za-z0-9_-]+}", api.APISessionRequired(getJobsByType)).Methods("GET")
}
func getJob(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobId()
if c.Err != nil {
return
}
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), job.Type)
if permissionRequired == nil {
c.Err = model.NewAppError("getJob", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
if err := json.NewEncoder(w).Encode(job); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func downloadJob(c *Context, w http.ResponseWriter, r *http.Request) {
config := c.App.Config()
const FilePath = "export"
const FileMime = "application/zip"
c.RequireJobId()
if c.Err != nil {
return
}
if !*config.MessageExportSettings.DownloadExportResults {
c.Err = model.NewAppError("downloadExportResultsNotEnabled", "app.job.download_export_results_not_enabled", nil, "", http.StatusNotImplemented)
return
}
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
// Currently, this endpoint only supports downloading the compliance report.
// If you need to download another job type, you will need to alter this section of the code to accommodate it.
if job.Type == model.JobTypeMessageExport && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionDownloadComplianceExportResult) {
c.SetPermissionError(model.PermissionDownloadComplianceExportResult)
return
} else if job.Type != model.JobTypeMessageExport {
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
isDownloadable, _ := strconv.ParseBool(job.Data["is_downloadable"])
if !isDownloadable {
c.Err = model.NewAppError("unableToDownloadJob", "api.job.unable_to_download_job", nil, "", http.StatusBadRequest)
return
}
fileName := job.Id + ".zip"
filePath := filepath.Join(FilePath, fileName)
fileReader, err := c.App.FileReader(filePath)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
return
}
defer fileReader.Close()
// We are able to pass 0 for content size due to the fact that Golang's serveContent (https://golang.org/src/net/http/fs.go)
// already sets that for us
web.WriteFileResponse(fileName, FileMime, 0, time.Unix(0, job.LastActivityAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, true, w, r)
}
func createJob(c *Context, w http.ResponseWriter, r *http.Request) {
var job model.Job
if jsonErr := json.NewDecoder(r.Body).Decode(&job); jsonErr != nil {
c.SetInvalidParamWithErr("job", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createJob", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameterAuditable(auditRec, "job", &job)
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), &job)
if permissionRequired == nil {
c.Err = model.NewAppError("unableToCreateJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
rjob, err := c.App.CreateJob(&job)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rjob)
auditRec.AddEventObjectType("job")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rjob); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getJobs(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Err != nil {
return
}
var validJobTypes []string
for _, jobType := range model.AllJobTypes {
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), jobType)
if permissionRequired == nil {
mlog.Warn("The job types of a job you are trying to retrieve does not contain permissions", mlog.String("jobType", jobType))
continue
}
if hasPermission {
validJobTypes = append(validJobTypes, jobType)
}
}
if len(validJobTypes) == 0 {
c.SetPermissionError()
return
}
jobs, appErr := c.App.GetJobsByTypesPage(validJobTypes, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(jobs)
if err != nil {
c.Err = model.NewAppError("getJobs", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getJobsByType(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobType()
if c.Err != nil {
return
}
hasPermission, permissionRequired := c.App.SessionHasPermissionToReadJob(*c.AppContext.Session(), c.Params.JobType)
if permissionRequired == nil {
c.Err = model.NewAppError("getJobsByType", "api.job.retrieve.nopermissions", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
jobs, appErr := c.App.GetJobsByTypePage(c.Params.JobType, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(jobs)
if err != nil {
c.Err = model.NewAppError("getJobsByType", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func cancelJob(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireJobId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("cancelJob", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "job_id", c.Params.JobId)
job, err := c.App.GetJob(c.Params.JobId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(job)
auditRec.AddEventObjectType("job")
// if permission to create, permission to cancel, same permission
hasPermission, permissionRequired := c.App.SessionHasPermissionToCreateJob(*c.AppContext.Session(), job)
if permissionRequired == nil {
c.Err = model.NewAppError("unableToCancelJob", "api.job.unable_to_create_job.incorrect_job_type", nil, "", http.StatusBadRequest)
return
}
if !hasPermission {
c.SetPermissionError(permissionRequired)
return
}
if err := c.App.CancelJob(c.Params.JobId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}

12
server/channels/api4/job_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitJobLocal() {
api.BaseRoutes.Jobs.Handle("", api.APILocal(getJobs)).Methods("GET")
api.BaseRoutes.Jobs.Handle("", api.APILocal(createJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}", api.APILocal(getJob)).Methods("GET")
api.BaseRoutes.Jobs.Handle("/{job_id:[A-Za-z0-9]+}/cancel", api.APILocal(cancelJob)).Methods("POST")
api.BaseRoutes.Jobs.Handle("/type/{job_type:[A-Za-z0-9_-]+}", api.APILocal(getJobsByType)).Methods("GET")
}

337
server/channels/api4/job_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,337 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestCreateJob(t *testing.T) {
th := Setup(t)
th.LoginSystemManager()
defer th.TearDown()
job := &model.Job{
Type: model.JobTypeActiveUsers,
Data: map[string]string{
"thing": "stuff",
},
}
t.Run("valid job as user without permissions", func(t *testing.T) {
_, resp, err := th.SystemManagerClient.CreateJob(job)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("valid job as user with permissions", func(t *testing.T) {
received, _, err := th.SystemAdminClient.CreateJob(job)
require.NoError(t, err)
defer th.App.Srv().Store().Job().Delete(received.Id)
})
t.Run("invalid job type as user without permissions", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.CreateJob(&model.Job{Type: model.NewId()})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
}
func TestGetJob(t *testing.T) {
th := Setup(t)
defer th.TearDown()
job := &model.Job{
Id: model.NewId(),
Status: model.JobStatusPending,
Type: model.JobTypeMessageExport,
}
_, err := th.App.Srv().Store().Job().Save(job)
require.NoError(t, err)
defer th.App.Srv().Store().Job().Delete(job.Id)
received, _, err := th.SystemAdminClient.GetJob(job.Id)
require.NoError(t, err)
require.Equal(t, job.Id, received.Id, "incorrect job received")
require.Equal(t, job.Status, received.Status, "incorrect job received")
_, resp, err := th.SystemAdminClient.GetJob("1234")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = th.Client.GetJob(job.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = th.SystemAdminClient.GetJob(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}
func TestGetJobs(t *testing.T) {
th := Setup(t)
defer th.TearDown()
jobType := model.JobTypeDataRetention
t0 := model.GetMillis()
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: t0 + 1,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: t0,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: t0 + 2,
},
}
for _, job := range jobs {
_, err := th.App.Srv().Store().Job().Save(job)
require.NoError(t, err)
defer th.App.Srv().Store().Job().Delete(job.Id)
}
received, _, err := th.SystemAdminClient.GetJobs(0, 2)
require.NoError(t, err)
require.Len(t, received, 2, "received wrong number of jobs")
require.Equal(t, jobs[2].Id, received[0].Id, "should've received newest job first")
require.Equal(t, jobs[0].Id, received[1].Id, "should've received second newest job second")
received, _, err = th.SystemAdminClient.GetJobs(1, 2)
require.NoError(t, err)
require.Equal(t, jobs[1].Id, received[0].Id, "should've received oldest job last")
_, resp, err := th.Client.GetJobs(0, 60)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestGetJobsByType(t *testing.T) {
th := Setup(t)
th.LoginSystemManager()
defer th.TearDown()
jobType := model.JobTypeDataRetention
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1000,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 999,
},
{
Id: model.NewId(),
Type: jobType,
CreateAt: 1001,
},
{
Id: model.NewId(),
Type: model.NewId(),
CreateAt: 1002,
},
}
for _, job := range jobs {
_, err := th.App.Srv().Store().Job().Save(job)
require.NoError(t, err)
defer th.App.Srv().Store().Job().Delete(job.Id)
}
received, _, err := th.SystemAdminClient.GetJobsByType(jobType, 0, 2)
require.NoError(t, err)
require.Len(t, received, 2, "received wrong number of jobs")
require.Equal(t, jobs[2].Id, received[0].Id, "should've received newest job first")
require.Equal(t, jobs[0].Id, received[1].Id, "should've received second newest job second")
received, _, err = th.SystemAdminClient.GetJobsByType(jobType, 1, 2)
require.NoError(t, err)
require.Len(t, received, 1, "received wrong number of jobs")
require.Equal(t, jobs[1].Id, received[0].Id, "should've received oldest job last")
_, resp, err := th.SystemAdminClient.GetJobsByType("", 0, 60)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, resp, err = th.SystemAdminClient.GetJobsByType(strings.Repeat("a", 33), 0, 60)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = th.Client.GetJobsByType(jobType, 0, 60)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = th.SystemManagerClient.GetJobsByType(model.JobTypeElasticsearchPostIndexing, 0, 60)
require.NoError(t, err)
}
func TestDownloadJob(t *testing.T) {
th := Setup(t).InitBasic()
th.LoginSystemManager()
defer th.TearDown()
jobName := model.NewId()
job := &model.Job{
Id: jobName,
Type: model.JobTypeMessageExport,
Data: map[string]string{
"export_type": "csv",
},
Status: model.JobStatusSuccess,
}
// DownloadExportResults is not set to true so we should get a not implemented error status
_, resp, err := th.Client.DownloadJob(job.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.MessageExportSettings.DownloadExportResults = true
})
// Normal user cannot download the results of these job (non-existent job)
_, resp, err = th.Client.DownloadJob(job.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// System admin trying to download the results of a non-existent job
_, resp, err = th.SystemAdminClient.DownloadJob(job.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// Here we have a job that exist in our database but the results do not exist therefore when we try to download the results
// as a system admin, we should get a not found status.
_, err = th.App.Srv().Store().Job().Save(job)
require.NoError(t, err)
defer th.App.Srv().Store().Job().Delete(job.Id)
filePath := "./data/export/" + job.Id + "/testdat.txt"
mkdirAllErr := os.MkdirAll(filepath.Dir(filePath), 0770)
require.NoError(t, mkdirAllErr)
os.Create(filePath)
// Normal user cannot download the results of these job (not the right permission)
_, resp, err = th.Client.DownloadJob(job.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.SystemManagerClient.DownloadJob(job.Id)
// System manager with default permissions cannot download the results of these job (Doesn't have correct permissions)
_, resp, err = th.SystemManagerClient.DownloadJob(job.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = th.SystemAdminClient.DownloadJob(job.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
job.Data["is_downloadable"] = "true"
updateStatus, err := th.App.Srv().Store().Job().UpdateOptimistically(job, model.JobStatusSuccess)
require.True(t, updateStatus)
require.NoError(t, err)
_, resp, err = th.SystemAdminClient.DownloadJob(job.Id)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
// Now we stub the results of the job into the same directory and try to download it again
// This time we should successfully retrieve the results without any error
filePath = "./data/export/" + job.Id + ".zip"
mkdirAllErr = os.MkdirAll(filepath.Dir(filePath), 0770)
require.NoError(t, mkdirAllErr)
os.Create(filePath)
_, _, err = th.SystemAdminClient.DownloadJob(job.Id)
require.NoError(t, err)
// Here we are creating a new job which doesn't have type of message export
jobName = model.NewId()
job = &model.Job{
Id: jobName,
Type: model.JobTypeCloud,
Data: map[string]string{
"export_type": "csv",
},
Status: model.JobStatusSuccess,
}
_, err = th.App.Srv().Store().Job().Save(job)
require.NoError(t, err)
defer th.App.Srv().Store().Job().Delete(job.Id)
// System admin shouldn't be able to download since the job type is not message export
_, resp, err = th.SystemAdminClient.DownloadJob(job.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}
func TestCancelJob(t *testing.T) {
th := Setup(t)
defer th.TearDown()
jobType := model.JobTypeMessageExport
jobs := []*model.Job{
{
Id: model.NewId(),
Type: jobType,
Status: model.JobStatusPending,
},
{
Id: model.NewId(),
Type: jobType,
Status: model.JobStatusInProgress,
},
{
Id: model.NewId(),
Type: jobType,
Status: model.JobStatusSuccess,
},
}
for _, job := range jobs {
_, err := th.App.Srv().Store().Job().Save(job)
require.NoError(t, err)
defer th.App.Srv().Store().Job().Delete(job.Id)
}
resp, err := th.Client.CancelJob(jobs[0].Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = th.SystemAdminClient.CancelJob(jobs[0].Id)
require.NoError(t, err)
_, err = th.SystemAdminClient.CancelJob(jobs[1].Id)
require.NoError(t, err)
resp, err = th.SystemAdminClient.CancelJob(jobs[2].Id)
require.Error(t, err)
CheckInternalErrorStatus(t, resp)
resp, err = th.SystemAdminClient.CancelJob(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
}

453
server/channels/api4/ldap.go Обычный файл
Просмотреть файл

@@ -0,0 +1,453 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"mime/multipart"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type mixedUnlinkedGroup struct {
Id *string `json:"mattermost_group_id"`
DisplayName string `json:"name"`
RemoteId string `json:"primary_key"`
HasSyncables *bool `json:"has_syncables"`
}
func (api *API) InitLdap() {
api.BaseRoutes.LDAP.Handle("/sync", api.APISessionRequired(syncLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/test", api.APISessionRequired(testLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/migrateid", api.APISessionRequired(migrateIdLdap)).Methods("POST")
// GET /api/v4/ldap/groups?page=0&per_page=1000
api.BaseRoutes.LDAP.Handle("/groups", api.APISessionRequired(getLdapGroups)).Methods("GET")
// POST /api/v4/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(linkLdapGroup)).Methods("POST")
// DELETE /api/v4/ldap/groups/:remote_id/link
api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/users/{user_id}/group_sync_memberships", api.APISessionRequired(addUserToGroupSyncables)).Methods("POST")
}
func syncLdap(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.syncLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
type LdapSyncOptions struct {
IncludeRemovedMembers bool `json:"include_removed_members"`
}
var opts LdapSyncOptions
err := json.NewDecoder(r.Body).Decode(&opts)
if err != nil {
c.Logger.Warn("Error decoding LDAP sync options", mlog.Err(err))
}
auditRec := c.MakeAuditRecord("syncLdap", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreateLdapSyncJob) {
c.SetPermissionError(model.PermissionCreateLdapSyncJob)
return
}
c.App.SyncLdap(opts.IncludeRemovedMembers)
auditRec.Success()
ReturnStatusOK(w)
}
func testLdap(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.testLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestLdap) {
c.SetPermissionError(model.PermissionTestLdap)
return
}
if err := c.App.TestLdap(); err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func getLdapGroups(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementGroups)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getLdapGroups", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
opts := model.LdapGroupSearchOpts{
Q: c.Params.Q,
}
if c.Params.IsLinked != nil {
opts.IsLinked = c.Params.IsLinked
}
if c.Params.IsConfigured != nil {
opts.IsConfigured = c.Params.IsConfigured
}
groups, total, appErr := c.App.GetAllLdapGroupsPage(c.Params.Page, c.Params.PerPage, opts)
if appErr != nil {
c.Err = appErr
return
}
mugs := []*mixedUnlinkedGroup{}
for _, group := range groups {
mug := &mixedUnlinkedGroup{
DisplayName: group.DisplayName,
RemoteId: group.GetRemoteId(),
}
if len(group.Id) == 26 {
mug.Id = &group.Id
mug.HasSyncables = &group.HasSyncables
}
mugs = append(mugs, mug)
}
b, err := json.Marshal(struct {
Count int `json:"count"`
Groups []*mixedUnlinkedGroup `json:"groups"`
}{Count: total, Groups: mugs})
if err != nil {
c.Err = model.NewAppError("Api4.getLdapGroups", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func linkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
auditRec := c.MakeAuditRecord("linkLdapGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
ldapGroup, appErr := c.App.GetLdapGroup(c.Params.RemoteId)
if appErr != nil {
c.Err = appErr
return
}
if ldapGroup == nil {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.ldap_group.not_found", nil, "", http.StatusNotFound)
return
}
group, appErr := c.App.GetGroupByRemoteID(ldapGroup.GetRemoteId(), model.GroupSourceLdap)
if appErr != nil && appErr.Id != "app.group.no_rows" {
c.Err = appErr
return
}
if group != nil {
audit.AddEventParameterAuditable(auditRec, "group", group)
}
var status int
var newOrUpdatedGroup *model.Group
// Truncate display name if necessary
var displayName string
if len(ldapGroup.DisplayName) > model.GroupDisplayNameMaxLength {
displayName = ldapGroup.DisplayName[:model.GroupDisplayNameMaxLength]
} else {
displayName = ldapGroup.DisplayName
}
// Group has been previously linked
if group != nil {
if group.DeleteAt == 0 {
newOrUpdatedGroup = group
} else {
group.DeleteAt = 0
group.DisplayName = displayName
group.RemoteId = ldapGroup.RemoteId
newOrUpdatedGroup, appErr = c.App.UpdateGroup(group)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newOrUpdatedGroup)
auditRec.AddEventObjectType("group")
}
status = http.StatusOK
} else {
// Group has never been linked
//
// For group mentions implementation, the Name column will no longer be set by default.
// Instead it will be set and saved in the web app when Group Mentions is enabled.
newGroup := &model.Group{
DisplayName: displayName,
RemoteId: ldapGroup.RemoteId,
Source: model.GroupSourceLdap,
}
newOrUpdatedGroup, appErr = c.App.CreateGroup(newGroup)
if appErr != nil {
c.Err = appErr
return
}
auditRec.AddEventResultState(newOrUpdatedGroup)
auditRec.AddEventObjectType("group")
status = http.StatusCreated
}
b, err := json.Marshal(newOrUpdatedGroup)
if err != nil {
c.Err = model.NewAppError("Api4.linkLdapGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
auditRec.Success()
w.WriteHeader(status)
w.Write(b)
}
func unlinkLdapGroup(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireRemoteId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("unlinkLdapGroup", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "remote_id", c.Params.RemoteId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.unlinkLdapGroup", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
group, err := c.App.GetGroupByRemoteID(c.Params.RemoteId, model.GroupSourceLdap)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(group)
auditRec.AddEventObjectType("group")
if group.DeleteAt == 0 {
deletedGroup, err := c.App.DeleteGroup(group.Id)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(deletedGroup)
}
auditRec.Success()
ReturnStatusOK(w)
}
func migrateIdLdap(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.StringInterfaceFromJSON(r.Body)
toAttribute, ok := props["toAttribute"].(string)
if !ok || toAttribute == "" {
c.SetInvalidParam("toAttribute")
return
}
auditRec := c.MakeAuditRecord("idMigrateLdap", audit.Fail)
audit.AddEventParameter(auditRec, "to_attribute", toAttribute)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAP {
c.Err = model.NewAppError("Api4.idMigrateLdap", "api.ldap_groups.license_error", nil, "", http.StatusNotImplemented)
return
}
if err := c.App.MigrateIdLDAP(toAttribute); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func parseLdapCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
err := r.ParseMultipartForm(maxFileSize)
if err != nil {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.parseform.app_error", nil, err.Error(), http.StatusBadRequest)
}
m := r.MultipartForm
fileArray, ok := m.File["certificate"]
if !ok {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
}
if len(fileArray) <= 0 {
return nil, model.NewAppError("addLdapCertificate", "api.admin.add_certificate.array.app_error", nil, "", http.StatusBadRequest)
}
return fileArray[0], nil
}
func addLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPublicCert) {
c.SetPermissionError(model.PermissionAddLdapPublicCert)
return
}
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addLdapPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddLdapPublicCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func addLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionAddLdapPrivateCert) {
c.SetPermissionError(model.PermissionAddLdapPrivateCert)
return
}
fileData, err := parseLdapCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
auditRec := c.MakeAuditRecord("addLdapPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
if err := c.App.AddLdapPrivateCertificate(fileData); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeLdapPublicCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPublicCert) {
c.SetPermissionError(model.PermissionRemoveLdapPublicCert)
return
}
auditRec := c.MakeAuditRecord("removeLdapPublicCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveLdapPublicCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func removeLdapPrivateCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveLdapPrivateCert) {
c.SetPermissionError(model.PermissionRemoveLdapPrivateCert)
return
}
auditRec := c.MakeAuditRecord("removeLdapPrivateCertificate", audit.Fail)
defer c.LogAuditRec(auditRec)
if err := c.App.RemoveLdapPrivateCertificate(); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
// addUserToGroupSyncables creates memberships—for the given user—to all of their group syncables (i.e. channels or teams).
// For each group the user is a member of, for each channel and/or team that group is associated with, the user will be added.
func addUserToGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) {
c.SetPermissionError(model.PermissionSysconsoleWriteUserManagementGroups)
return
}
user, appErr := c.App.GetUser(c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
if user.AuthService != model.UserAuthServiceLdap {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.user.add_user_to_group_syncables.not_ldap_user.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("addUserToGroupSyncables", audit.Fail)
defer c.LogAuditRec(auditRec)
params := model.CreateDefaultMembershipParams{Since: 0, ReAddRemovedMembers: true, ScopedUserID: &user.Id}
err := c.App.CreateDefaultMemberships(c.AppContext, params)
if err != nil {
c.Err = model.NewAppError("addUserToGroupSyncables", "api.admin.syncables_error", nil, err.Error(), http.StatusBadRequest)
return
}
auditRec.Success()
ReturnStatusOK(w)
}

16
server/channels/api4/ldap_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitLdapLocal() {
api.BaseRoutes.LDAP.Handle("/migrateid", api.APILocal(migrateIdLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/sync", api.APILocal(syncLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/test", api.APILocal(testLdap)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/groups", api.APILocal(getLdapGroups)).Methods("GET")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APILocal(addLdapPublicCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APILocal(addLdapPrivateCertificate)).Methods("POST")
api.BaseRoutes.LDAP.Handle("/certificate/public", api.APILocal(removeLdapPublicCertificate)).Methods("DELETE")
api.BaseRoutes.LDAP.Handle("/certificate/private", api.APILocal(removeLdapPrivateCertificate)).Methods("DELETE")
}

310
server/channels/api4/ldap_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,310 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces/mocks"
)
var spPrivateKey = `-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDbVbUfO8gFDgqx
w3Z7gX5layTKKXQT623h0eUHXo95jIdApMyCdhRYoYz9OUvo01aQ0UyErcyWKUJE
3E0YEP/MjvBGTIemmkj/NQWtLqIxZZFnl8uVcm5gPWTJgEhzy9i4/D49qolYakJO
VkK+fnAWUzIiO5GIM6It8zuDIK9a8lnLK6CGWhWUDR8s6nlxOmiG32LRKPAOJrlx
NPbDJO5SV/Wkte/1UdVCR9cW5FroJ5ae/cUEpMeNpiFMCc49gDPEOLOTAroYs1bO
hS4mGArlO0WZUz37cyZSo/MtWJo2Y7bkVejAt6pdMcmvYNy5yddrslA+0OiteZS4
dN01tHa4QiEaNVZ+DdKWfpJFYqqVNNq/YMveUjk7IbnnJpz+ylOc8zNneoiwE5CI
+mmFp0X0+Zt1IJD7BXZEw37Jhk+YeBdQUnkHPWKHj4dkKPpfjPX/K1r2G1CY7iDG
3V1fPsIFAfCUvLbWH994haezz9U+hXu89LmhnKq638fDduGYKQOyYz8/BsQ1MQP5
kCrDg5HhnqUx/dECElFlHCnq3Z/gHoQOicxA8f1GeCDiIE2VFYZRQLDL21lb9ozQ
BFbLZZfGaLGmUPhecQ0RrQ/W4YPNhBvyXELOjCsDfu6ltnob6E8Lux7sNohFuLaY
g0AzDRfezhU0RqWXURKlpiqG0qaoWwIDAQABAoICAQDXt4vTlDA9CHpsKxm0jr+J
b79XNT38+Wew2YavoMjretLrOSoKhaetI/ZOdrO54WEaPT9MnsLATQPoReNs8Asl
XM/j1BD2QnfYyIU0ttC+VG6VvC12Zn04GimuJIUdnjcgeLWeYMOEOb3M3fn28NO8
oUaFdKDFnEK9fqPha5wLjp/Ruq6+dIsUeXNX8aRPQGrde4bsv56ZzGxGcxjfBMuA
IRJvVKEUXc+oyI867IycF5OD+4Jx9r5tCh9lcZ9tzVEcg8fZpqzw7jFKHKIuxSay
HYFuMvia/b2LOcRJrQK+y4NtPzETmY/s6LK70kBEWceNHGrf3Qd61kD2yblmwH6h
F47M/tY8OAXoSmxS259HzJc7DT1WvaDiCZzfVntoJPv6x7CaP6XfLySAq3MTP77x
jGIVZYMg9lGQBTQE6SHCuoM/szUT6PYRtbrcpqjh/MOHvALzgjgtAXWrDf8zLRpD
RAAOKjBILIgNC92h3Oe9bFFfRMEkWvDYWeUs2tmEVJtZm7lDB02vVcRyvRk1sFy3
BkDNB+INbZX/aDblFl8Z7W60jOa7Wr+Hn68dds56PYzsl5NxNTL3fFlx8Yaztd6b
3j654bXGiYSKLPn2PGatWdNcmIsFXN5UIKEDHrn/YeiagFoNvPL1AzpyVvzbkKp0
g+HWAssgI7TTQ3fRMtolgQKCAQEA+B7cdp2k41mKmKrDdmj4iS4ES/SR133ED0SJ
F3fVcJPyKv7iW2zTl8vwTE817HBavPX01bBah51ZSI0RZv+VL11hsGFZfKKYIX5t
60v5zKk5Z+WKlAyM/BHs43gej4KKrd5SMxma/cXpCNdgRJjz8YJpEuoI14Tq7qXC
Bi1v1GLrGXOLng8Mklh7rgs0pwF7BZIzur1xtAKDztebhofrLTXLmLZS/DkHI5qY
qeMonrm5MI/B66FiQEsVt+guz4fMAeNp/sLUPk2iL/qGFyDjvXOosHChffNDv2+l
A17X/oKGpd3jahXRrP/UeuuVyVt5B5xA+SCbzJHF87A0pnKTWQKCAQEA4kzT2lou
vToJxJZWM92TN+1kOfN3VIq5yWpOcesd2NOnVf9SwmSYf/KKsyvzcrMXWSIL8Gp3
h5eBK69N0bHkWfSkGTFa9WwrXx1yR3IOir1L+iFhd6Z8ASvwK93QIBYTSyE3eK9d
RU3ahXIQJFifx1tNoU8RbhlgLukaovnfQjt9xI67cgvXrb9RA0d8hZ81r8Lg/uz4
PN5htNCe6YWC01c2ufIGOqwO6QoYYW3yR00L1ANkE1ohHSrz7JGKthS8vdK/Ogfh
UwR/JaA3kZ6DdoWAfzZd1BbT3WgMG36Il6Hk2EtOCYuD0AuURWcQjJGkN4+xWqtS
U+bfB11bUBgm0wKCAQBnStm226vwJa+oHLbgjZSh7zFEuZ0ZW7cKMBruVSnbAww2
0ANF0klIEVOJQRSOyLtNnQr/Brq5aEzqAigze8UMgdCQUAaj90Bj+TEjWm60v+Ix
GYMWXR84NPIsRC5cyhiXh00rDsbSTNjVoGvoQtCTQxohEKL7rc7r6L+cOMAsZ729
y7dc5qDyL7nVW77go6ImUJYOcJ1sNfvPWTzaxaynFpUajxR/AfKx5MMXPoUDhwfM
apxtTrMLVvbEp/kM1liclKLktxEKmuEhHidCa6PDk+mvAkSInYQfpwfIHmzG/Gm3
lWb+G/U9EwfO4FJsEBOTkn4N+IBDqpABAeL5RAuJAoIBAHFi9z9Psl2DqANFJFoG
ak46dt6Ge8LzY1VlG3r+yEys+AohzRCzoKlzGEXf/rH4w/kYEw1Z+xwIMGN4CbDI
xlbAOjyZOy7/DNgyg+ECaADiCiCA+zodQ8K+hi8ki7SX+wDI2udwTnZ8JMJ6PVZI
xX345HOvj1cwBb5bc8o3EsM31bNXpNnmzyEyW+AdwGmfNSIkreFtUJAHCMO1R/pP
uBY2e6g9eRuKvEnNkhu3IA7TrtqC/HCp1y+rJt7gqbTDvTILV183NZIIDcEHfvBK
kSogiBq1Xdv3uB4WlQJtqvj22Bf721Ty/4+NTbRciLE2BCcGq2F3t99sLVGeWDNQ
dpsCggEAcuxrYqR659hvqAhjFjfiOY3X5VMzaWJO7ERDCgVjtVsopBOaM23Gg9zl
4TISwG3MXBjDwOqhpP7T6ytxWZphyN51zXgwGghhcze8f+HstGo0dpjnFSM5ml+Y
q0o8LMYlM6NrtYwocMTm4fzh9gXa6aDGadb/dW8DsWmYmBHXH5ViZB7uzbcbtQRI
7EuwV+DYLualVpJ99pjbb7a8PPPvQrGLb2Lhlk7P2NT25Nal26vwUTPHTZVV4s7W
0HY6fD+opKhBHQami5XbSUVznTWus6Zgc3bi4k9NsSNUQNfBKz79zM/EvIPXEklP
kSU80FrXITorOgZogkDk0FVpJA3qvQ==
-----END PRIVATE KEY-----`
var spPublicCertificate = `-----BEGIN CERTIFICATE-----
MIIFijCCA3KgAwIBAgIJAIRQ3EwrvOprMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRIwEAYDVQQHDAlQYWxvIEFsdG8xEzARBgNVBAoMCk1hdHRlcm1vc3Qx
DzANBgNVBAsMBkRldk9wczETMBEGA1UEAwwKY2xpZW50LmNvbTAeFw0xOTA5MTIx
NzM1MzdaFw0yOTA5MDkxNzM1MzdaMFwxCzAJBgNVBAYTAlVTMRIwEAYDVQQHDAlQ
YWxvIEFsdG8xEzARBgNVBAoMCk1hdHRlcm1vc3QxDzANBgNVBAsMBkRldk9wczET
MBEGA1UEAwwKY2xpZW50LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBANtVtR87yAUOCrHDdnuBfmVrJMopdBPrbeHR5Qdej3mMh0CkzIJ2FFihjP05
S+jTVpDRTIStzJYpQkTcTRgQ/8yO8EZMh6aaSP81Ba0uojFlkWeXy5VybmA9ZMmA
SHPL2Lj8Pj2qiVhqQk5WQr5+cBZTMiI7kYgzoi3zO4Mgr1ryWcsroIZaFZQNHyzq
eXE6aIbfYtEo8A4muXE09sMk7lJX9aS17/VR1UJH1xbkWugnlp79xQSkx42mIUwJ
zj2AM8Q4s5MCuhizVs6FLiYYCuU7RZlTPftzJlKj8y1YmjZjtuRV6MC3ql0xya9g
3LnJ12uyUD7Q6K15lLh03TW0drhCIRo1Vn4N0pZ+kkViqpU02r9gy95SOTshuecm
nP7KU5zzM2d6iLATkIj6aYWnRfT5m3UgkPsFdkTDfsmGT5h4F1BSeQc9YoePh2Qo
+l+M9f8rWvYbUJjuIMbdXV8+wgUB8JS8ttYf33iFp7PP1T6Fe7z0uaGcqrrfx8N2
4ZgpA7JjPz8GxDUxA/mQKsODkeGepTH90QISUWUcKerdn+AehA6JzEDx/UZ4IOIg
TZUVhlFAsMvbWVv2jNAEVstll8ZosaZQ+F5xDRGtD9bhg82EG/JcQs6MKwN+7qW2
ehvoTwu7Huw2iEW4tpiDQDMNF97OFTRGpZdREqWmKobSpqhbAgMBAAGjTzBNMBIG
A1UdEwEB/wQIMAYBAf8CAQAwNwYDVR0RBDAwLoIOd3d3LmNsaWVudC5jb22CEGFk
bWluLmNsaWVudC5jb22HBMCoAQqHBAoAAOowDQYJKoZIhvcNAQELBQADggIBAFEI
D1ySRS+lQYVm24PPIUH5OmBEJUsVKI/zUXEQ4hdqEqN4UA3NGKkujajTz2fStaOj
LfGDup1ZQRYG6VVvNwbZHX9G9mb8TyZ12XFLVjPTbxoG+NZb3ipue9S6qZcT9WEF
sjaXhkVNhhVc1GOMnv/FNiclLPWLMnR8WST+Y+WSsT59wP40kJynaT7wQt2TmImg
kQfM69jQNgAkyrFwO8y1YcnH7Avrw9YvzhUWG2FfNCTTVNb+StxNtqGwvDV33iZ2
bBUWIy2fsNUA4tUYK31Ye6thJiKmvy/LqVJ415gPsI3zHzTCLU/GBUCNCNnEDnhU
KO2K3mk1wK3sshMGcda/Xz2a9TfkIxs0pkenS57bZ8xT7mxBzXsZGm7Mnb2fujmX
fBEyxQ2ot0Nl9Lp26WrBjQZojJ10Ic2IRxU3spC/FYK7BenQEAdnNHkyQ3lowAto
NpOL+j+1ooksPQbp4DeIBbrZDNKvFot+ja2aDJ738sgXf8ht7kGXA5DPNtPLsmUr
wpZrhxKD6pXVPhA6EeG2efdUP1ODslmehl4t2yX+FqHChnl7E012W8Cf0Ugybp1t
15IXg8GxCRENSNAwpOvTMkoonHqNvBkaCDZHtxeyJMJWQW1B0Xek1JY3CNHvnY7I
MCOV5SHi05kD42JSSbmw190VAa4QRGikaeWRhDsj
-----END CERTIFICATE-----`
func TestTestLdap(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err := client.TestLdap()
CheckNotImplementedStatus(t, resp)
require.Error(t, err)
CheckErrorID(t, err, "api.ldap_groups.license_error")
})
th.App.Srv().SetLicense(model.NewTestLicense("ldap_groups"))
resp, err := th.Client.TestLdap()
CheckForbiddenStatus(t, resp)
require.Error(t, err)
CheckErrorID(t, err, "api.context.permissions.app_error")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err = client.TestLdap()
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
require.Error(t, err)
CheckErrorID(t, err, "ent.ldap.disabled.app_error")
})
}
func TestSyncLdap(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err := client.TestLdap()
CheckNotImplementedStatus(t, resp)
require.Error(t, err)
CheckErrorID(t, err, "api.ldap_groups.license_error")
})
th.App.Srv().SetLicense(model.NewTestLicense("ldap_groups"))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.LdapSettings.EnableSync = true
})
ldapMock := &mocks.LdapInterface{}
mockCall := ldapMock.On(
"StartSynchronizeJob",
mock.AnythingOfType("bool"),
mock.AnythingOfType("bool"),
).Return(nil, nil)
ready := make(chan bool)
includeRemovedMembers := false
mockCall.RunFn = func(args mock.Arguments) {
includeRemovedMembers = args[1].(bool)
ready <- true
}
th.App.Channels().Ldap = ldapMock
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.SyncLdap(false)
<-ready
require.NoError(t, err)
require.False(t, includeRemovedMembers)
_, err = client.SyncLdap(true)
<-ready
require.NoError(t, err)
require.True(t, includeRemovedMembers)
})
resp, err := th.Client.SyncLdap(false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
}
func TestGetLdapGroups(t *testing.T) {
th := Setup(t)
defer th.TearDown()
_, resp, err := th.Client.GetLdapGroups()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, resp, err := client.GetLdapGroups()
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
}
func TestLinkLdapGroup(t *testing.T) {
const entryUUID string = "foo"
th := Setup(t)
defer th.TearDown()
_, resp, err := th.Client.LinkLdapGroup(entryUUID)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = th.SystemAdminClient.LinkLdapGroup(entryUUID)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestUnlinkLdapGroup(t *testing.T) {
const entryUUID string = "foo"
th := Setup(t)
defer th.TearDown()
_, resp, err := th.Client.UnlinkLdapGroup(entryUUID)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = th.SystemAdminClient.UnlinkLdapGroup(entryUUID)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestMigrateIdLdap(t *testing.T) {
th := Setup(t)
defer th.TearDown()
resp, err := th.Client.MigrateIdLdap("objectGUID")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
resp, err = client.MigrateIdLdap("")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.MigrateIdLdap("objectGUID")
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
})
}
func TestUploadPublicCertificate(t *testing.T) {
th := Setup(t)
defer th.TearDown()
_, err := th.Client.UploadLdapPublicCertificate([]byte(spPublicCertificate))
require.Error(t, err, "Should have failed. No System Admin privileges")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err = client.UploadLdapPublicCertificate([]byte(spPrivateKey))
require.NoErrorf(t, err, "Should have passed. System Admin privileges %v", err)
})
_, err = th.Client.DeleteLdapPublicCertificate()
require.Error(t, err, "Should have failed. No System Admin privileges")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.DeleteLdapPublicCertificate()
require.NoError(t, err, "Should have passed. System Admin privileges")
})
}
func TestUploadPrivateCertificate(t *testing.T) {
th := Setup(t)
defer th.TearDown()
_, err := th.Client.UploadLdapPrivateCertificate([]byte(spPrivateKey))
require.Error(t, err, "Should have failed. No System Admin privileges")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err = client.UploadLdapPrivateCertificate([]byte(spPrivateKey))
require.NoErrorf(t, err, "Should have passed. System Admin privileges %v", err)
})
_, err = th.Client.DeleteLdapPrivateCertificate()
require.Error(t, err, "Should have failed. No System Admin privileges")
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
_, err := client.DeleteLdapPrivateCertificate()
require.NoErrorf(t, err, "Should have passed. System Admin privileges %v", err)
})
}
func TestAddUserToGroupSyncables(t *testing.T) {
th := Setup(t)
defer th.TearDown()
resp, err := th.Client.AddUserToGroupSyncables(th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = th.SystemAdminClient.AddUserToGroupSyncables("invalid-user-id")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
resp, err = th.SystemAdminClient.AddUserToGroupSyncables(th.BasicUser.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
id := model.NewId()
user := &model.User{
Email: "test@localhost",
Username: model.NewId(),
AuthData: &id,
AuthService: model.UserAuthServiceLdap,
}
user, err = th.App.Srv().Store().User().Save(user)
require.NoError(t, err)
resp, err = th.SystemAdminClient.AddUserToGroupSyncables(user.Id)
require.NoError(t, err)
CheckOKStatus(t, resp)
}

403
server/channels/api4/license.go Обычный файл
Просмотреть файл

@@ -0,0 +1,403 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
)
func (api *API) InitLicense() {
api.BaseRoutes.APIRoot.Handle("/trial-license", api.APISessionRequired(requestTrialLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/review", api.APISessionRequired(requestTrueUpReview)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license/review/status", api.APISessionRequired(trueUpReviewStatus)).Methods("GET")
}
func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
if format == "" {
c.Err = model.NewAppError("getClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
return
}
if format != "old" {
c.SetInvalidParam("format")
return
}
var clientLicense map[string]string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
clientLicense = c.App.Srv().ClientLicense()
} else {
clientLicense = c.App.Srv().GetSanitizedClientLicense()
}
w.Write([]byte(model.MapToJSON(clientLicense)))
}
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("addLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("addLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
fileArray, ok := m.File["license"]
if !ok {
c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileArray) <= 0 {
c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest)
return
}
fileData := fileArray[0]
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
file, err := fileData.Open()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
licenseBytes := buf.Bytes()
license, appErr := utils.LicenseValidator.LicenseFromBytes(licenseBytes)
if appErr != nil {
c.Err = appErr
return
}
// skip the restrictions if license is a sanctioned trial
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
lm := c.App.Srv().Platform().LicenseManager()
if lm == nil {
c.Err = model.NewAppError("addLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusInternalServerError)
return
}
canStartTrialLicense, err := lm.CanStartTrial()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, "", http.StatusInternalServerError)
return
}
if !canStartTrialLicense {
c.Err = model.NewAppError("addLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
return
}
}
license, appErr = c.App.Srv().SaveLicense(licenseBytes)
if appErr != nil {
if appErr.Id == model.ExpiredLicenseError {
c.LogAudit("failed - expired or non-started license")
} else if appErr.Id == model.InvalidLicenseError {
c.LogAudit("failed - invalid license")
} else {
c.LogAudit("failed - unable to save license")
}
c.Err = appErr
return
}
if c.App.Channels().License().IsCloud() {
// If cloud, invalidate the caches when a new license is loaded
defer c.App.Srv().Cloud.HandleLicenseChange()
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(license); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("removeLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("removeLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if err := c.App.Srv().RemoveLicense(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestTrialLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("requestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
if c.App.Srv().Platform().LicenseManager() == nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
canStartTrialLicense, err := c.App.Srv().Platform().LicenseManager().CanStartTrial()
if err != nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.error", nil, err.Error(), http.StatusInternalServerError)
return
}
if !canStartTrialLicense {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.can-start-trial.not-allowed", nil, "", http.StatusBadRequest)
return
}
var trialRequest struct {
Users int `json:"users"`
TermsAccepted bool `json:"terms_accepted"`
ReceiveEmailsAccepted bool `json:"receive_emails_accepted"`
}
b, readErr := io.ReadAll(r.Body)
if readErr != nil {
c.Err = model.NewAppError("requestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
return
}
json.Unmarshal(b, &trialRequest)
if err := c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func requestRenewalLink(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("requestRenewalLink", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("requestRenewalLink", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
renewalLink, token, err := c.App.Srv().GenerateLicenseRenewalLink()
if err != nil {
c.Err = err
return
}
if c.App.Cloud() == nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
// check if it is possible to renew license on the portal with generated token
status, e := c.App.Cloud().GetLicenseSelfServeStatus(c.AppContext.Session().UserId, token)
if e != nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, e.Error(), http.StatusInternalServerError)
return
}
if !status.IsRenewable {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.cannot_renew_on_cws", nil, "License is not self-serve renewable", http.StatusBadRequest)
return
}
auditRec.Success()
c.LogAudit("success")
_, werr := w.Write([]byte(fmt.Sprintf(`{"renewal_link": "%s"}`, renewalLink)))
if werr != nil {
c.Err = model.NewAppError("requestRenewalLink", "api.license.request_renewal_link.app_error", nil, werr.Error(), http.StatusForbidden)
return
}
}
func getPrevTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Srv().Platform().LicenseManager() == nil {
c.Err = model.NewAppError("getPrevTrialLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusForbidden)
return
}
license, err := c.App.Srv().Platform().LicenseManager().GetPrevTrial()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var clientLicense map[string]string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
clientLicense = utils.GetClientLicense(license)
} else {
clientLicense = utils.GetSanitizedClientLicense(utils.GetClientLicense(license))
}
w.Write([]byte(model.MapToJSON(clientLicense)))
}
func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.not_allowed_for_cloud", nil, "", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
return
}
// If a true up review has already been submitted for the current due date, complete the request
// with no errors.
if status.Completed {
ReturnStatusOK(w)
}
profileMap, err := c.App.GetTrueUpProfile()
if err != nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "", http.StatusInternalServerError)
return
}
profileMapJson, err := json.Marshal(profileMap)
if err != nil {
c.SetJSONEncodingError(err)
return
}
// Do not send true-up review data if the user has already requested one for the quarter.
// And only send a true-up review via as a one-time telemetry request if telemetry is disabled.
telemetryEnabled := c.App.Config().LogSettings.EnableDiagnostics
if telemetryEnabled != nil && !*telemetryEnabled {
// Send telemetry data
c.App.Srv().GetTelemetryService().SendTelemetry(model.TrueUpReviewTelemetryName, profileMap)
// Update the review status to reflect the completion.
status.Completed = true
c.App.Srv().Store().TrueUpReview().Update(status)
}
// Encode to string rather than byte[] otherwise json.Marshal will encode it further.
encodedData := b64.StdEncoding.EncodeToString(profileMapJson)
responseContent := struct {
Content string `json:"content"`
}{Content: encodedData}
response, _ := json.Marshal(responseContent)
w.Write(response)
}
func trueUpReviewStatus(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
// Check for license
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.license_required", nil, "True up review requires a license", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.not_allowed_for_cloud", nil, "True up review is not allowed for cloud instances", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
}
json, err := json.Marshal(status)
if err != nil {
c.Err = model.NewAppError("trueUpReviewStatus", "api.marshal_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}

94
server/channels/api4/license_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,94 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"io"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitLicenseLocal() {
api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localRemoveLicense)).Methods("DELETE")
}
func localAddLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localAddLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
fileArray, ok := m.File["license"]
if !ok {
c.Err = model.NewAppError("addLicense", "api.license.add_license.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(fileArray) <= 0 {
c.Err = model.NewAppError("addLicense", "api.license.add_license.array.app_error", nil, "", http.StatusBadRequest)
return
}
fileData := fileArray[0]
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
file, err := fileData.Open()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
license, appErr := c.App.Srv().SaveLicense(buf.Bytes())
if appErr != nil {
if appErr.Id == model.ExpiredLicenseError {
c.LogAudit("failed - expired or non-started license")
} else if appErr.Id == model.InvalidLicenseError {
c.LogAudit("failed - invalid license")
} else {
c.LogAudit("failed - unable to save license")
}
c.Err = appErr
return
}
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(license); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func localRemoveLicense(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("localRemoveLicense", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if err := c.App.Srv().RemoveLicense(); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}

404
server/channels/api4/license_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,404 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/einterfaces/mocks"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
mocks2 "github.com/mattermost/mattermost-server/v6/server/channels/utils/mocks"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/testutils"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestGetOldClientLicense(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
license, _, err := client.GetOldClientLicense("")
require.NoError(t, err)
require.NotEqual(t, license["IsLicensed"], "", "license not returned correctly")
client.Logout()
_, _, err = client.GetOldClientLicense("")
require.NoError(t, err)
resp, err := client.DoAPIGet("/license/client", "")
require.Error(t, err, "get /license/client did not return an error")
require.Equal(t, http.StatusBadRequest, resp.StatusCode,
"expected 400 bad request")
resp, err = client.DoAPIGet("/license/client?format=junk", "")
require.Error(t, err, "get /license/client?format=junk did not return an error")
require.Equal(t, http.StatusBadRequest, resp.StatusCode,
"expected 400 Bad Request")
license, _, err = th.SystemAdminClient.GetOldClientLicense("")
require.NoError(t, err)
require.NotEmpty(t, license["IsLicensed"], "license not returned correctly")
}
func TestUploadLicenseFile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
LocalClient := th.LocalClient
t.Run("as system user", func(t *testing.T) {
resp, err := client.UploadLicenseFile([]byte{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
resp, err := c.UploadLicenseFile([]byte{})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
}, "as system admin user")
t.Run("as restricted system admin user", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
resp, err := th.SystemAdminClient.UploadLicenseFile([]byte{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("restricted admin setting not honoured through local client", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
resp, err := LocalClient.UploadLicenseFile([]byte{})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("server has already gone through trial", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = false })
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
userCount := 100
mills := model.GetMillis()
license := model.License{
Id: "AAAAAAAAAAAAAAAAAAAAAAAAAA",
Features: &model.Features{
Users: &userCount,
},
Customer: &model.Customer{
Name: "Test",
},
StartsAt: mills + 100,
ExpiresAt: mills + 100 + (30*(time.Hour*24) + (time.Hour * 8)).Milliseconds(),
}
mockLicenseValidator.On("LicenseFromBytes", mock.Anything).Return(&license, nil).Once()
licenseBytes, _ := json.Marshal(license)
licenseStr := string(licenseBytes)
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, licenseStr)
utils.LicenseValidator = &mockLicenseValidator
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(false, nil).Once()
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
resp, err := th.SystemAdminClient.UploadLicenseFile([]byte("sadasdasdasdasdasdsa"))
CheckErrorID(t, err, "api.license.request-trial.can-start-trial.not-allowed")
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("try to get gone through trial, with TE build", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = false })
th.App.Srv().Platform().SetLicenseManager(nil)
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
license := model.License{
Id: model.NewId(),
Features: &model.Features{
Users: model.NewInt(100),
},
Customer: &model.Customer{
Name: "Test",
},
StartsAt: model.GetMillis() + 100,
ExpiresAt: model.GetMillis() + 100 + (30*(time.Hour*24) + (time.Hour * 8)).Milliseconds(),
}
mockLicenseValidator.On("LicenseFromBytes", mock.Anything).Return(&license, nil).Once()
licenseBytes, err := json.Marshal(license)
require.NoError(t, err)
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseBytes))
utils.LicenseValidator = &mockLicenseValidator
resp, err := th.SystemAdminClient.UploadLicenseFile([]byte(""))
CheckErrorID(t, err, "api.license.upgrade_needed.app_error")
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
})
t.Run("allow uploading sanctioned trials even if server already gone through trial", func(t *testing.T) {
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
userCount := 100
mills := model.GetMillis()
license := model.License{
Id: "PPPPPPPPPPPPPPPPPPPPPPPPPP",
Features: &model.Features{
Users: &userCount,
},
Customer: &model.Customer{
Name: "Test",
},
IsTrial: true,
StartsAt: mills + 100,
ExpiresAt: mills + 100 + (29*(time.Hour*24) + (time.Hour * 8)).Milliseconds(),
}
mockLicenseValidator.On("LicenseFromBytes", mock.Anything).Return(&license, nil).Once()
licenseBytes, _ := json.Marshal(license)
licenseStr := string(licenseBytes)
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, licenseStr)
utils.LicenseValidator = &mockLicenseValidator
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(false, nil).Once()
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
resp, err := th.SystemAdminClient.UploadLicenseFile([]byte("sadasdasdasdasdasdsa"))
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
})
}
func TestRemoveLicenseFile(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
LocalClient := th.LocalClient
t.Run("as system user", func(t *testing.T) {
resp, err := client.RemoveLicenseFile()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) {
_, err := c.RemoveLicenseFile()
require.NoError(t, err)
}, "as system admin user")
t.Run("as restricted system admin user", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
resp, err := th.SystemAdminClient.RemoveLicenseFile()
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("restricted admin setting not honoured through local client", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
_, err := LocalClient.RemoveLicenseFile()
require.NoError(t, err)
})
}
func TestRequestTrialLicense(t *testing.T) {
th := Setup(t)
defer th.TearDown()
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(true, nil)
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065/" })
t.Run("permission denied", func(t *testing.T) {
resp, err := th.Client.RequestTrialLicense(1000)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("trial license user count less than current users", func(t *testing.T) {
nUsers := 1
license := model.NewTestLicense()
license.Features.Users = model.NewInt(nUsers)
licenseJSON, jsonErr := json.Marshal(license)
require.NoError(t, jsonErr)
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
response := map[string]string{
"license": string(licenseJSON),
}
err := json.NewEncoder(res).Encode(response)
require.NoError(t, err)
}))
defer testServer.Close()
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON))
utils.LicenseValidator = &mockLicenseValidator
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(true, nil).Once()
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL })
defer func(requestTrialURL string) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL })
}(originalCwsUrl)
resp, err := th.SystemAdminClient.RequestTrialLicense(nUsers)
CheckErrorID(t, err, "api.license.add_license.unique_users.app_error")
CheckBadRequestStatus(t, resp)
})
t.Run("returns status 451 when it receives status 451", func(t *testing.T) {
nUsers := 1
license := model.NewTestLicense()
license.Features.Users = model.NewInt(nUsers)
licenseJSON, jsonErr := json.Marshal(license)
require.NoError(t, jsonErr)
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusUnavailableForLegalReasons)
}))
defer testServer.Close()
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON))
utils.LicenseValidator = &mockLicenseValidator
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(true, nil).Once()
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL })
defer func(requestTrialURL string) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL })
}(originalCwsUrl)
resp, err := th.SystemAdminClient.RequestTrialLicense(nUsers)
require.Error(t, err)
require.Equal(t, resp.StatusCode, 451)
})
th.App.Srv().Platform().SetLicenseManager(nil)
t.Run("trial license should fail if LicenseManager is nil", func(t *testing.T) {
resp, err := th.SystemAdminClient.RequestTrialLicense(1)
CheckErrorID(t, err, "api.license.upgrade_needed.app_error")
CheckForbiddenStatus(t, resp)
})
}
func TestRequestRenewalLink(t *testing.T) {
th := Setup(t)
defer th.TearDown()
require.NotPanics(t, func() {
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = nil
resp, err := th.SystemAdminClient.DoAPIGet("/license/renewal", "")
CheckErrorID(t, err, "app.license.generate_renewal_token.no_license")
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
func TestRequestTrueUpReview(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense())
t.Run("returns status 200 when telemetry data sent", func(t *testing.T) {
resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("returns 501 when ran by cloud user", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
th.App.Srv().SetLicense(model.NewTestLicense())
})
t.Run("returns 403 when user does not have permissions", func(t *testing.T) {
resp, err := th.Client.DoAPIPost("/license/review", "")
require.Error(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("returns 400 when license is nil", func(t *testing.T) {
th.App.Srv().SetLicense(nil)
resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
}
func TestTrueUpReviewStatus(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense())
t.Run("returns 200 when status retrieved", func(t *testing.T) {
resp, err := th.SystemAdminClient.DoAPIGet("/license/review/status", "")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("returns 501 when ran by cloud user", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
resp, err := th.SystemAdminClient.DoAPIGet("/license/review/status", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
th.App.Srv().SetLicense(model.NewTestLicense())
})
t.Run("returns 403 when user does not have permissions", func(t *testing.T) {
resp, err := th.Client.DoAPIGet("/license/review/status", "")
require.Error(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("returns 400 when license is nil", func(t *testing.T) {
th.App.Srv().SetLicense(nil)
resp, err := th.SystemAdminClient.DoAPIGet("/license/review/status", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
}

31
server/channels/api4/main_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"flag"
"testing"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
)
var replicaFlag bool
func TestMain(m *testing.M) {
if f := flag.Lookup("mysql-replica"); f == nil {
flag.BoolVar(&replicaFlag, "mysql-replica", false, "")
flag.Parse()
}
var options = testlib.HelperOptions{
EnableStore: true,
EnableResources: true,
WithReadReplica: replicaFlag,
}
mainHelper = testlib.NewMainHelperWithOptions(&options)
defer mainHelper.Close()
mainHelper.Main(m)
}

57
server/channels/api4/notify_admin.go Обычный файл
Просмотреть файл

@@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) {
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
err := json.NewDecoder(r.Body).Decode(&notifyAdminRequest)
if err != nil {
c.SetInvalidParamWithErr("notifyAdminRequest", err)
return
}
userId := c.AppContext.Session().UserId
appErr := c.App.SaveAdminNotification(userId, notifyAdminRequest)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableAPITriggerAdminNotifications {
c.Err = model.NewAppError("Api4.handleTriggerNotifyAdminPosts", "api.cloud.app_error", nil, "Manual triggering of notifications not allowed", http.StatusForbidden)
return
}
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
err := json.NewDecoder(r.Body).Decode(&notifyAdminRequest)
if err != nil {
c.SetInvalidParamWithErr("notifyAdminRequest", err)
return
}
// only system admins can manually trigger these notifications
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
appErr := c.App.SendNotifyAdminPosts(c.AppContext, "", "", notifyAdminRequest.TrialNotification)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}

156
server/channels/api4/notify_admin_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,156 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestNotifyAdmin(t *testing.T) {
t.Run("error when plan is unknown when notifying on upgrade", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: "Unknown plan",
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
})
require.Error(t, err)
require.Equal(t, err.Error(), ": Unable to save notify data.")
require.Equal(t, http.StatusInternalServerError, statusCode)
})
t.Run("error when plan is unknown when notifying to trial", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: "Unknown plan",
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
TrialNotification: true,
})
require.Error(t, err)
require.Equal(t, err.Error(), ": Unable to save notify data.")
require.Equal(t, http.StatusInternalServerError, statusCode)
})
t.Run("error when feature is unknown when notifying on upgrade", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: "Unknown feature",
})
require.Error(t, err)
require.Equal(t, err.Error(), ": Unable to save notify data.")
require.Equal(t, http.StatusInternalServerError, statusCode)
})
t.Run("error when feature is unknown when notifying to trial", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: "Unknown feature",
TrialNotification: true,
})
require.Error(t, err)
require.Equal(t, err.Error(), ": Unable to save notify data.")
require.Equal(t, http.StatusInternalServerError, statusCode)
})
t.Run("error when user tries to notify again on same feature within the cool off period", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, statusCode)
// second attempt to notify for all professional features
statusCode, err = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
})
require.Error(t, err)
require.Equal(t, err.Error(), ": Already notified admin")
require.Equal(t, http.StatusForbidden, statusCode)
})
t.Run("successfully save upgrade notification", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, statusCode)
})
}
func TestTriggerNotifyAdmin(t *testing.T) {
t.Run("error when EnableAPITriggerAdminNotifications is not true", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = false })
statusCode, err := th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
require.Error(t, err)
require.Equal(t, err.Error(), ": Internal error during cloud api request.")
require.Equal(t, http.StatusForbidden, statusCode)
})
t.Run("error when non admins try to trigger notifications", func(t *testing.T) {
th := Setup(t).InitBasic().InitLogin()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
statusCode, err := th.Client.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
require.Error(t, err)
require.Equal(t, err.Error(), ": You do not have the appropriate permissions.")
require.Equal(t, http.StatusForbidden, statusCode)
})
t.Run("happy path", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, statusCode)
statusCode, err = th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
require.NoError(t, err)
require.Equal(t, http.StatusOK, statusCode)
})
}

312
server/channels/api4/oauth.go Обычный файл
Просмотреть файл

@@ -0,0 +1,312 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitOAuth() {
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(createOAuthApp)).Methods("POST")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(updateOAuthApp)).Methods("PUT")
api.BaseRoutes.OAuthApps.Handle("", api.APISessionRequired(getOAuthApps)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(getOAuthApp)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("/info", api.APISessionRequired(getOAuthAppInfo)).Methods("GET")
api.BaseRoutes.OAuthApp.Handle("", api.APISessionRequired(deleteOAuthApp)).Methods("DELETE")
api.BaseRoutes.OAuthApp.Handle("/regen_secret", api.APISessionRequired(regenerateOAuthAppSecret)).Methods("POST")
api.BaseRoutes.User.Handle("/oauth/apps/authorized", api.APISessionRequired(getAuthorizedOAuthApps)).Methods("GET")
}
func createOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
var oauthApp model.OAuthApp
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
c.SetInvalidParamWithErr("oauth_app", jsonErr)
return
}
auditRec := c.MakeAuditRecord("createOAuthApp", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
oauthApp.IsTrusted = false
}
oauthApp.CreatorId = c.AppContext.Session().UserId
rapp, err := c.App.CreateOAuthApp(&oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.Success()
auditRec.AddEventResultState(rapp)
auditRec.AddEventObjectType("oauth_app")
c.LogAudit("client_id=" + rapp.Id)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rapp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updateOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updateOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
var oauthApp model.OAuthApp
if jsonErr := json.NewDecoder(r.Body).Decode(&oauthApp); jsonErr != nil {
c.SetInvalidParamWithErr("oauth_app", jsonErr)
return
}
audit.AddEventParameterAuditable(auditRec, "oauth_app", &oauthApp)
// The app being updated in the payload must be the same one as indicated in the URL.
if oauthApp.Id != c.Params.AppId {
c.SetInvalidParam("app_id")
return
}
oldOAuthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oldOAuthApp)
if c.AppContext.Session().UserId != oldOAuthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
oauthApp.IsTrusted = oldOAuthApp.IsTrusted
}
updatedOAuthApp, err := c.App.UpdateOAuthApp(oldOAuthApp, &oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(updatedOAuthApp)
auditRec.AddEventObjectType("oauth_app")
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(updatedOAuthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.Err = model.NewAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "", http.StatusForbidden)
return
}
var apps []*model.OAuthApp
var appErr *model.AppError
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
apps, appErr = c.App.GetOAuthApps(c.Params.Page, c.Params.PerPage)
} else if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
apps, appErr = c.App.GetOAuthAppsByCreator(c.AppContext.Session().UserId, c.Params.Page, c.Params.PerPage)
} else {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(apps)
if err != nil {
c.Err = model.NewAppError("getOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
oauthApp.Sanitize()
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deleteOAuthApp", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oauthApp)
auditRec.AddEventObjectType("oauth_app")
if c.AppContext.Session().UserId != oauthApp.CreatorId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
err = c.App.DeleteOAuthApp(oauthApp.Id)
if err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("success")
ReturnStatusOK(w)
}
func regenerateOAuthAppSecret(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireAppId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("regenerateOAuthAppSecret", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "oauth_app_id", c.Params.AppId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageOAuth) {
c.SetPermissionError(model.PermissionManageOAuth)
return
}
oauthApp, err := c.App.GetOAuthApp(c.Params.AppId)
if err != nil {
c.Err = err
return
}
auditRec.AddEventPriorState(oauthApp)
auditRec.AddEventObjectType("oauth_app")
if oauthApp.CreatorId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystemWideOAuth) {
c.SetPermissionError(model.PermissionManageSystemWideOAuth)
return
}
oauthApp, err = c.App.RegenerateOAuthAppSecret(oauthApp)
if err != nil {
c.Err = err
return
}
auditRec.AddEventResultState(oauthApp)
auditRec.Success()
c.LogAudit("success")
if err := json.NewEncoder(w).Encode(oauthApp); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getAuthorizedOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
apps, appErr := c.App.GetAuthorizedAppsForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(apps)
if err != nil {
c.Err = model.NewAppError("getAuthorizedOAuthApps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}

618
server/channels/api4/oauth_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,618 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestCreateOAuthApp(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
defaultRolePermissions := th.SaveDefaultRolePermissions()
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}, IsTrusted: true}
rapp, resp, err := adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
assert.Equal(t, oapp.Name, rapp.Name, "names did not match")
assert.Equal(t, oapp.IsTrusted, rapp.IsTrusted, "trusted did no match")
// Revoke permission from regular users.
th.RemovePermissionFromRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
_, resp, err = client.CreateOAuthApp(oapp)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
rapp, resp, err = client.CreateOAuthApp(oapp)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
assert.False(t, rapp.IsTrusted, "trusted should be false - created by non admin")
oapp.Name = ""
_, resp, err = adminClient.CreateOAuthApp(oapp)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
r, err := client.DoAPIPost("/oauth/apps", "garbage")
require.Error(t, err, "expected error from garbage post")
assert.Equal(t, http.StatusBadRequest, r.StatusCode)
client.Logout()
_, resp, err = client.CreateOAuthApp(oapp)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
oapp.Name = GenerateTestAppName()
_, resp, err = adminClient.CreateOAuthApp(oapp)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestUpdateOAuthApp(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
defaultRolePermissions := th.SaveDefaultRolePermissions()
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{
Name: "oapp",
IsTrusted: false,
IconURL: "https://nowhere.com/img",
Homepage: "https://nowhere.com",
Description: "test",
CallbackUrls: []string{"https://callback.com"},
}
oapp, _, _ = adminClient.CreateOAuthApp(oapp)
oapp.Name = "oapp_update"
oapp.IsTrusted = true
oapp.IconURL = "https://nowhere.com/img_update"
oapp.Homepage = "https://nowhere_update.com"
oapp.Description = "test_update"
oapp.CallbackUrls = []string{"https://callback_update.com", "https://another_callback.com"}
updatedApp, _, err := adminClient.UpdateOAuthApp(oapp)
require.NoError(t, err)
assert.Equal(t, oapp.Id, updatedApp.Id, "Id should have not updated")
assert.Equal(t, oapp.CreatorId, updatedApp.CreatorId, "CreatorId should have not updated")
assert.Equal(t, oapp.CreateAt, updatedApp.CreateAt, "CreateAt should have not updated")
assert.NotEqual(t, oapp.UpdateAt, updatedApp.UpdateAt, "UpdateAt should have updated")
assert.Equal(t, oapp.ClientSecret, updatedApp.ClientSecret, "ClientSecret should have not updated")
assert.Equal(t, oapp.Name, updatedApp.Name, "Name should have updated")
assert.Equal(t, oapp.Description, updatedApp.Description, "Description should have updated")
assert.Equal(t, oapp.IconURL, updatedApp.IconURL, "IconURL should have updated")
if len(updatedApp.CallbackUrls) == len(oapp.CallbackUrls) {
for i, callbackURL := range updatedApp.CallbackUrls {
assert.Equal(t, oapp.CallbackUrls[i], callbackURL, "Description should have updated")
}
}
assert.Equal(t, oapp.Homepage, updatedApp.Homepage, "Homepage should have updated")
assert.Equal(t, oapp.IsTrusted, updatedApp.IsTrusted, "IsTrusted should have updated")
th.LoginBasic2()
updatedApp.CreatorId = th.BasicUser2.Id
_, resp, err := client.UpdateOAuthApp(oapp)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.LoginBasic()
// Revoke permission from regular users.
th.RemovePermissionFromRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
_, resp, err = client.UpdateOAuthApp(oapp)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
oapp.Id = "zhk9d1ggatrqz236c7h87im7bc"
_, resp, err = adminClient.UpdateOAuthApp(oapp)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
_, resp, err = adminClient.UpdateOAuthApp(oapp)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
client.Logout()
_, resp, err = client.UpdateOAuthApp(oapp)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
oapp.Id = "junk"
_, resp, err = adminClient.UpdateOAuthApp(oapp)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.LoginBasic()
userOapp := &model.OAuthApp{
Name: "useroapp",
IsTrusted: false,
IconURL: "https://nowhere.com/img",
Homepage: "https://nowhere.com",
Description: "test",
CallbackUrls: []string{"https://callback.com"},
}
userOapp, _, err = client.CreateOAuthApp(userOapp)
require.NoError(t, err)
userOapp.IsTrusted = true
userOapp, _, err = client.UpdateOAuthApp(userOapp)
require.NoError(t, err)
assert.False(t, userOapp.IsTrusted)
userOapp.IsTrusted = true
userOapp, _, err = adminClient.UpdateOAuthApp(userOapp)
require.NoError(t, err)
assert.True(t, userOapp.IsTrusted)
userOapp.IsTrusted = false
userOapp, _, err = client.UpdateOAuthApp(userOapp)
require.NoError(t, err)
assert.True(t, userOapp.IsTrusted)
}
func TestGetOAuthApps(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
defaultRolePermissions := th.SaveDefaultRolePermissions()
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
rapp, _, err := adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
oapp.Name = GenerateTestAppName()
rapp2, _, err := client.CreateOAuthApp(oapp)
require.NoError(t, err)
apps, _, err := adminClient.GetOAuthApps(0, 1000)
require.NoError(t, err)
found1 := false
found2 := false
for _, a := range apps {
if a.Id == rapp.Id {
found1 = true
}
if a.Id == rapp2.Id {
found2 = true
}
}
assert.Truef(t, found1, "missing oauth app %v", rapp.Id)
assert.Truef(t, found2, "missing oauth app %v", rapp2.Id)
apps, _, err = adminClient.GetOAuthApps(1, 1)
require.NoError(t, err)
require.Equal(t, 1, len(apps), "paging failed")
apps, _, err = client.GetOAuthApps(0, 1000)
require.NoError(t, err)
require.True(t, len(apps) == 1 || apps[0].Id == rapp2.Id, "wrong apps returned")
// Revoke permission from regular users.
th.RemovePermissionFromRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
_, resp, err := client.GetOAuthApps(0, 1000)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
_, resp, err = client.GetOAuthApps(0, 1000)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
_, resp, err = adminClient.GetOAuthApps(0, 1000)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestGetOAuthApp(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
defaultRolePermissions := th.SaveDefaultRolePermissions()
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
rapp, _, err := adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
oapp.Name = GenerateTestAppName()
rapp2, _, err := client.CreateOAuthApp(oapp)
require.NoError(t, err)
rrapp, _, err := adminClient.GetOAuthApp(rapp.Id)
require.NoError(t, err)
assert.Equal(t, rapp.Id, rrapp.Id, "wrong app")
assert.NotEqual(t, "", rrapp.ClientSecret, "should not be sanitized")
rrapp2, _, err := adminClient.GetOAuthApp(rapp2.Id)
require.NoError(t, err)
assert.Equal(t, rapp2.Id, rrapp2.Id, "wrong app")
assert.NotEqual(t, "", rrapp2.ClientSecret, "should not be sanitized")
_, _, err = client.GetOAuthApp(rapp2.Id)
require.NoError(t, err)
_, resp, err := client.GetOAuthApp(rapp.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
// Revoke permission from regular users.
th.RemovePermissionFromRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
_, resp, err = client.GetOAuthApp(rapp2.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
_, resp, err = client.GetOAuthApp(rapp2.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, resp, err = adminClient.GetOAuthApp("junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = adminClient.GetOAuthApp(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
_, resp, err = adminClient.GetOAuthApp(rapp.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestGetOAuthAppInfo(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
defaultRolePermissions := th.SaveDefaultRolePermissions()
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
rapp, _, err := adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
oapp.Name = GenerateTestAppName()
rapp2, _, err := client.CreateOAuthApp(oapp)
require.NoError(t, err)
rrapp, _, err := adminClient.GetOAuthAppInfo(rapp.Id)
require.NoError(t, err)
assert.Equal(t, rapp.Id, rrapp.Id, "wrong app")
assert.Equal(t, "", rrapp.ClientSecret, "should be sanitized")
rrapp2, _, err := adminClient.GetOAuthAppInfo(rapp2.Id)
require.NoError(t, err)
assert.Equal(t, rapp2.Id, rrapp2.Id, "wrong app")
assert.Equal(t, "", rrapp2.ClientSecret, "should be sanitized")
_, _, err = client.GetOAuthAppInfo(rapp2.Id)
require.NoError(t, err)
_, _, err = client.GetOAuthAppInfo(rapp.Id)
require.NoError(t, err)
// Revoke permission from regular users.
th.RemovePermissionFromRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
_, _, err = client.GetOAuthAppInfo(rapp2.Id)
require.NoError(t, err)
client.Logout()
_, resp, err := client.GetOAuthAppInfo(rapp2.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, resp, err = adminClient.GetOAuthAppInfo("junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = adminClient.GetOAuthAppInfo(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
_, resp, err = adminClient.GetOAuthAppInfo(rapp.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestDeleteOAuthApp(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
defaultRolePermissions := th.SaveDefaultRolePermissions()
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
rapp, _, err := adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
oapp.Name = GenerateTestAppName()
rapp2, _, err := client.CreateOAuthApp(oapp)
require.NoError(t, err)
_, err = adminClient.DeleteOAuthApp(rapp.Id)
require.NoError(t, err)
_, err = adminClient.DeleteOAuthApp(rapp2.Id)
require.NoError(t, err)
rapp, _, err = adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
oapp.Name = GenerateTestAppName()
rapp2, _, err = client.CreateOAuthApp(oapp)
require.NoError(t, err)
resp, err := client.DeleteOAuthApp(rapp.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.DeleteOAuthApp(rapp2.Id)
require.NoError(t, err)
// Revoke permission from regular users.
th.RemovePermissionFromRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
resp, err = client.DeleteOAuthApp(rapp.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
resp, err = client.DeleteOAuthApp(rapp.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
resp, err = adminClient.DeleteOAuthApp("junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = adminClient.DeleteOAuthApp(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
resp, err = adminClient.DeleteOAuthApp(rapp.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestRegenerateOAuthAppSecret(t *testing.T) {
th := Setup(t)
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
defaultRolePermissions := th.SaveDefaultRolePermissions()
enableOAuthServiceProvider := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuthServiceProvider })
}()
// Grant permission to regular users.
th.AddPermissionToRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
rapp, _, err := adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
oapp.Name = GenerateTestAppName()
rapp2, _, err := client.CreateOAuthApp(oapp)
require.NoError(t, err)
rrapp, _, err := adminClient.RegenerateOAuthAppSecret(rapp.Id)
require.NoError(t, err)
assert.Equal(t, rrapp.Id, rapp.Id, "wrong app")
assert.NotEqual(t, rapp.ClientSecret, rrapp.ClientSecret, "secret didn't change")
_, _, err = adminClient.RegenerateOAuthAppSecret(rapp2.Id)
require.NoError(t, err)
rapp, _, err = adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
oapp.Name = GenerateTestAppName()
rapp2, _, err = client.CreateOAuthApp(oapp)
require.NoError(t, err)
_, resp, err := client.RegenerateOAuthAppSecret(rapp.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = client.RegenerateOAuthAppSecret(rapp2.Id)
require.NoError(t, err)
// Revoke permission from regular users.
th.RemovePermissionFromRole(model.PermissionManageOAuth.Id, model.SystemUserRoleId)
_, resp, err = client.RegenerateOAuthAppSecret(rapp.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
_, resp, err = client.RegenerateOAuthAppSecret(rapp.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, resp, err = adminClient.RegenerateOAuthAppSecret("junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = adminClient.RegenerateOAuthAppSecret(model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
_, resp, err = adminClient.RegenerateOAuthAppSecret(rapp.Id)
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}
func TestGetAuthorizedOAuthAppsForUser(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
adminClient := th.SystemAdminClient
enableOAuth := th.App.Config().ServiceSettings.EnableOAuthServiceProvider
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableOAuthServiceProvider = enableOAuth })
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
oapp := &model.OAuthApp{Name: GenerateTestAppName(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
rapp, _, err := adminClient.CreateOAuthApp(oapp)
require.NoError(t, err)
authRequest := &model.AuthorizeRequest{
ResponseType: model.AuthCodeResponseType,
ClientId: rapp.Id,
RedirectURI: rapp.CallbackUrls[0],
Scope: "",
State: "123",
}
_, _, err = client.AuthorizeOAuthApp(authRequest)
require.NoError(t, err)
apps, _, err := client.GetAuthorizedOAuthAppsForUser(th.BasicUser.Id, 0, 1000)
require.NoError(t, err)
found := false
for _, a := range apps {
if a.Id == rapp.Id {
found = true
}
assert.Equal(t, "", a.ClientSecret, "not sanitized")
}
require.True(t, found, "missing app")
_, resp, err := client.GetAuthorizedOAuthAppsForUser(th.BasicUser2.Id, 0, 1000)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = client.GetAuthorizedOAuthAppsForUser("junk", 0, 1000)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
client.Logout()
_, resp, err = client.GetAuthorizedOAuthAppsForUser(th.BasicUser.Id, 0, 1000)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = adminClient.GetAuthorizedOAuthAppsForUser(th.BasicUser.Id, 0, 1000)
require.NoError(t, err)
}
func TestNilAuthorizeOAuthApp(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
_, _, err := client.AuthorizeOAuthApp(nil)
require.Error(t, err)
CheckErrorID(t, err, "api.context.invalid_body_param.app_error")
}

41
server/channels/api4/openGraph.go Обычный файл
Просмотреть файл

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitOpenGraph() {
api.BaseRoutes.OpenGraph.Handle("", api.APISessionRequired(getOpenGraphMetadata)).Methods("POST")
}
func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().ServiceSettings.EnableLinkPreviews {
c.Err = model.NewAppError("getOpenGraphMetadata", "api.post.link_preview_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
props := model.StringInterfaceFromJSON(r.Body)
url := ""
ok := false
if url, ok = props["url"].(string); url == "" || !ok {
c.SetInvalidParam("url")
return
}
buf, err := c.App.GetOpenGraphMetadata(url)
if err != nil {
mlog.Warn("GetOpenGraphMetadata request failed",
mlog.String("requestURL", url),
mlog.Err(err))
w.Write([]byte(`{"url": ""}`))
return
}
w.Write(buf)
}

76
server/channels/api4/openGraph_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGetOpenGraphMetadata(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
enableLinkPreviews := *th.App.Config().ServiceSettings.EnableLinkPreviews
allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
defer func() {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableLinkPreviews = enableLinkPreviews })
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableLinkPreviews = true })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
})
ogDataCacheMissCount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ogDataCacheMissCount++
if r.URL.Path == "/og-data/" {
fmt.Fprintln(w, `
<html><head><meta property="og:type" content="article" />
<meta property="og:title" content="Test Title" />
<meta property="og:url" content="http://example.com/" />
</head><body></body></html>
`)
} else if r.URL.Path == "/no-og-data/" {
fmt.Fprintln(w, `<html><head></head><body></body></html>`)
}
}))
for _, data := range [](map[string]any){
{"path": "/og-data/", "title": "Test Title", "cacheMissCount": 1},
{"path": "/no-og-data/", "title": "", "cacheMissCount": 2},
// Data should be cached for following
{"path": "/og-data/", "title": "Test Title", "cacheMissCount": 2},
{"path": "/no-og-data/", "title": "", "cacheMissCount": 2},
} {
openGraph, _, err := client.OpenGraph(ts.URL + data["path"].(string))
require.NoError(t, err)
require.Equalf(t, openGraph["title"], data["title"].(string),
"OG data title mismatch for path \"%s\".")
require.Equal(t, ogDataCacheMissCount, data["cacheMissCount"].(int),
"Cache miss count didn't match.")
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableLinkPreviews = false })
_, resp, err := client.OpenGraph(ts.URL + "/og-data/")
require.Error(t, err)
CheckNotImplementedStatus(t, resp)
}

34
server/channels/api4/permission.go Обычный файл
Просмотреть файл

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitPermissions() {
api.BaseRoutes.Permissions.Handle("/ancillary", api.APISessionRequired(appendAncillaryPermissions)).Methods("GET")
}
func appendAncillaryPermissions(c *Context, w http.ResponseWriter, r *http.Request) {
keys, ok := r.URL.Query()["subsection_permissions"]
if !ok || len(keys[0]) < 1 {
c.SetInvalidURLParam("subsection_permissions")
return
}
permissions := strings.Split(keys[0], ",")
b, err := json.Marshal(model.AddAncillaryPermissions(permissions))
if err != nil {
c.SetJSONEncodingError(err)
return
}
w.Write(b)
}

44
server/channels/api4/permissions_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGetAncillaryPermissions(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
var subsectionPermissions []string
var expectedAncillaryPermissions []string
t.Run("Valid Case, Passing in SubSection Permissions", func(t *testing.T) {
subsectionPermissions = []string{model.PermissionSysconsoleReadReportingSiteStatistics.Id}
expectedAncillaryPermissions = []string{model.PermissionGetAnalytics.Id}
actualAncillaryPermissions, _, err := th.Client.GetAncillaryPermissions(subsectionPermissions)
require.NoError(t, err)
assert.Equal(t, append(subsectionPermissions, expectedAncillaryPermissions...), actualAncillaryPermissions)
})
t.Run("Invalid Case, Passing in SubSection Permissions That Don't Exist", func(t *testing.T) {
subsectionPermissions = []string{"All", "The", "Things", "She", "Said", "Running", "Through", "My", "Head"}
expectedAncillaryPermissions = []string{}
actualAncillaryPermissions, _, err := th.Client.GetAncillaryPermissions(subsectionPermissions)
require.NoError(t, err)
assert.Equal(t, append(subsectionPermissions, expectedAncillaryPermissions...), actualAncillaryPermissions)
})
t.Run("Invalid Case, Passing in nothing", func(t *testing.T) {
subsectionPermissions = []string{}
expectedAncillaryPermissions = []string{}
_, resp, err := th.Client.GetAncillaryPermissions(subsectionPermissions)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
}

473
server/channels/api4/plugin.go Обычный файл
Просмотреть файл

@@ -0,0 +1,473 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
MaximumPluginFileSize = 50 * 1024 * 1024
)
func (api *API) InitPlugin() {
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(getPlugins)).Methods("GET")
api.BaseRoutes.Plugin.Handle("", api.APISessionRequired(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugins.Handle("/install_from_url", api.APISessionRequired(installPluginFromURL)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(installMarketplacePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/statuses", api.APISessionRequired(getPluginStatuses)).Methods("GET")
api.BaseRoutes.Plugin.Handle("/enable", api.APISessionRequired(enablePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/disable", api.APISessionRequired(disablePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/webapp", api.APIHandler(getWebappPlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APISessionRequired(getMarketplacePlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APIHandler(setFirstAdminVisitMarketplaceStatus)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace/first_admin_visit", api.APISessionRequired(getFirstAdminVisitMarketplaceStatus)).Methods("GET")
}
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
config := c.App.Config()
if !*config.PluginSettings.Enable || !*config.PluginSettings.EnableUploads || *config.PluginSettings.RequirePluginSignature {
c.Err = model.NewAppError("uploadPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("uploadPlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := r.ParseMultipartForm(MaximumPluginFileSize); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
m := r.MultipartForm
pluginArray, ok := m.File["plugin"]
if !ok {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(pluginArray) <= 0 {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.array.app_error", nil, "", http.StatusBadRequest)
return
}
audit.AddEventParameter(auditRec, "filename", pluginArray[0].Filename)
file, err := pluginArray[0].Open()
if err != nil {
c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest)
return
}
defer file.Close()
force := false
if len(m.Value["force"]) > 0 && m.Value["force"][0] == "true" {
force = true
}
installPlugin(c, w, file, force)
auditRec.Success()
}
func installPluginFromURL(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable ||
*c.App.Config().PluginSettings.RequirePluginSignature ||
!*c.App.Config().PluginSettings.EnableUploads {
c.Err = model.NewAppError("installPluginFromURL", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("installPluginFromURL", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
force, _ := strconv.ParseBool(r.URL.Query().Get("force"))
downloadURL := r.URL.Query().Get("plugin_download_url")
audit.AddEventParameter(auditRec, "url", downloadURL)
pluginFileBytes, err := c.App.DownloadFromURL(downloadURL)
if err != nil {
c.Err = model.NewAppError("installPluginFromURL", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
installPlugin(c, w, bytes.NewReader(pluginFileBytes), force)
auditRec.Success()
}
func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().PluginSettings.EnableMarketplace {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("installMarketplacePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
pluginRequest, err := model.PluginRequestFromReader(r.Body)
if err != nil {
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_plugin_request.app_error", nil, err.Error(), http.StatusNotImplemented)
return
}
audit.AddEventParameter(auditRec, "plugin_id", pluginRequest.Id)
// Always install the latest compatible version
// https://mattermost.atlassian.net/browse/MM-41981
pluginRequest.Version = ""
manifest, appErr := c.App.Channels().InstallMarketplacePlugin(pluginRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddMeta("plugin_name", manifest.Name)
auditRec.AddMeta("plugin_desc", manifest.Description)
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(manifest); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
response, err := c.App.GetPlugins()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPluginStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
response, err := c.App.GetClusterPluginStatuses()
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(response); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("removePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
err := c.App.Channels().RemovePlugin(c.Params.PluginId)
if err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getWebappPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
manifests, appErr := c.App.GetActivePluginManifests()
if appErr != nil {
c.Err = appErr
return
}
clientManifests := []*model.Manifest{}
for _, m := range manifests {
if m.HasClient() {
manifest := m.ClientManifest()
// There is no reason to expose the SettingsSchema in this API call; it's not used in the webapp.
manifest.SettingsSchema = nil
clientManifests = append(clientManifests, manifest)
}
}
js, err := json.Marshal(clientManifests)
if err != nil {
c.Err = model.NewAppError("getWebappPlugins", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func getMarketplacePlugins(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
if !*c.App.Config().PluginSettings.EnableMarketplace {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
filter, err := parseMarketplacePluginFilter(r.URL)
if err != nil {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
// if we are looking for remote only, we don't need to check for permissions
if !filter.RemoteOnly && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadPlugins) {
c.SetPermissionError(model.PermissionSysconsoleReadPlugins)
return
}
plugins, appErr := c.App.GetMarketplacePlugins(filter)
if appErr != nil {
c.Err = appErr
return
}
json, err := json.Marshal(plugins)
if err != nil {
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(json)
}
func enablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("activatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("enablePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := c.App.EnablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func disablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePluginId()
if c.Err != nil {
return
}
if !*c.App.Config().PluginSettings.Enable {
c.Err = model.NewAppError("deactivatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return
}
auditRec := c.MakeAuditRecord("disablePlugin", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "plugin_id", c.Params.PluginId)
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWritePlugins) {
c.SetPermissionError(model.PermissionSysconsoleWritePlugins)
return
}
if err := c.App.DisablePlugin(c.Params.PluginId); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, error) {
page, err := parseInt(u, "page", 0)
if err != nil {
return nil, err
}
perPage, err := parseInt(u, "per_page", 100)
if err != nil {
return nil, err
}
filter := u.Query().Get("filter")
serverVersion := u.Query().Get("server_version")
localOnly, _ := strconv.ParseBool(u.Query().Get("local_only"))
remoteOnly, _ := strconv.ParseBool(u.Query().Get("remote_only"))
if localOnly && remoteOnly {
return nil, errors.New("local_only and remote_only cannot be both true")
}
return &model.MarketplacePluginFilter{
Page: page,
PerPage: perPage,
Filter: filter,
ServerVersion: serverVersion,
LocalOnly: localOnly,
RemoteOnly: remoteOnly,
}, nil
}
func installPlugin(c *Context, w http.ResponseWriter, plugin io.ReadSeeker, force bool) {
manifest, appErr := c.App.InstallPlugin(plugin, force)
if appErr != nil {
c.Err = appErr
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(manifest); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func setFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("setFirstAdminVisitMarketplaceStatus", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminVisitMarketplaceObj := model.System{
Name: model.SystemFirstAdminVisitMarketplace,
Value: "true",
}
if err := c.App.Srv().Store().System().SaveOrUpdate(&firstAdminVisitMarketplaceObj); err != nil {
c.Err = model.NewAppError("setFirstAdminVisitMarketplaceStatus", "api.error_set_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
return
}
message := model.NewWebSocketEvent(model.WebsocketFirstAdminVisitMarketplaceStatusReceived, "", "", "", nil, "")
message.Add("firstAdminVisitMarketplaceStatus", firstAdminVisitMarketplaceObj.Value)
c.App.Publish(message)
auditRec.Success()
ReturnStatusOK(w)
}
func getFirstAdminVisitMarketplaceStatus(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("getFirstAdminVisitMarketplaceStatus", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageSystem)
return
}
firstAdminVisitMarketplaceObj, err := c.App.Srv().Store().System().GetByName(model.SystemFirstAdminVisitMarketplace)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
firstAdminVisitMarketplaceObj = &model.System{
Name: model.SystemFirstAdminVisitMarketplace,
Value: "false",
}
default:
c.Err = model.NewAppError("getFirstAdminVisitMarketplaceStatus", "api.error_get_first_admin_visit_marketplace_status", nil, err.Error(), http.StatusInternalServerError)
return
}
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(firstAdminVisitMarketplaceObj); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}

15
server/channels/api4/plugin_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitPluginLocal() {
api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("", api.APILocal(getPlugins)).Methods("GET")
api.BaseRoutes.Plugins.Handle("/install_from_url", api.APILocal(installPluginFromURL)).Methods("POST")
api.BaseRoutes.Plugin.Handle("", api.APILocal(removePlugin)).Methods("DELETE")
api.BaseRoutes.Plugin.Handle("/enable", api.APILocal(enablePlugin)).Methods("POST")
api.BaseRoutes.Plugin.Handle("/disable", api.APILocal(disablePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APILocal(installMarketplacePlugin)).Methods("POST")
api.BaseRoutes.Plugins.Handle("/marketplace", api.APILocal(getMarketplacePlugins)).Methods("GET")
}

1886
server/channels/api4/plugin_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

1124
server/channels/api4/post.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

10
server/channels/api4/post_local.go Обычный файл
Просмотреть файл

@@ -0,0 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
func (api *API) InitPostLocal() {
api.BaseRoutes.Post.Handle("", api.APILocal(getPost)).Methods("GET")
api.BaseRoutes.PostsForChannel.Handle("", api.APILocal(getPostsForChannel)).Methods("GET")
}

3799
server/channels/api4/post_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

164
server/channels/api4/preference.go Обычный файл
Просмотреть файл

@@ -0,0 +1,164 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitPreference() {
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(getPreferences)).Methods("GET")
api.BaseRoutes.Preferences.Handle("", api.APISessionRequired(updatePreferences)).Methods("PUT")
api.BaseRoutes.Preferences.Handle("/delete", api.APISessionRequired(deletePreferences)).Methods("POST")
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferencesByCategory)).Methods("GET")
api.BaseRoutes.Preferences.Handle("/{category:[A-Za-z0-9_]+}/name/{preference_name:[A-Za-z0-9_]+}", api.APISessionRequired(getPreferenceByCategoryAndName)).Methods("GET")
}
func getPreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferencesForUser(c.Params.UserId)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPreferencesByCategory(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireCategory()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferenceByCategoryForUser(c.Params.UserId, c.Params.Category)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getPreferenceByCategoryAndName(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId().RequireCategory().RequirePreferenceName()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
preferences, err := c.App.GetPreferenceByCategoryAndNameForUser(c.Params.UserId, c.Params.Category, c.Params.PreferenceName)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(preferences); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("updatePreferences", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var preferences model.Preferences
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
c.SetInvalidParamWithErr("preferences", jsonErr)
return
}
var sanitizedPreferences model.Preferences
for _, pref := range preferences {
if pref.Category == model.PreferenceCategoryFlaggedPost {
post, err := c.App.GetSinglePost(pref.Name, false)
if err != nil {
c.SetInvalidParam("preference.name")
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
sanitizedPreferences = append(sanitizedPreferences, pref)
}
if err := c.App.UpdatePreferences(c.Params.UserId, sanitizedPreferences); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("deletePreferences", audit.Fail)
defer c.LogAuditRec(auditRec)
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
var preferences model.Preferences
if jsonErr := json.NewDecoder(r.Body).Decode(&preferences); jsonErr != nil {
c.SetInvalidParamWithErr("preferences", jsonErr)
return
}
if err := c.App.DeletePreferences(c.Params.UserId, preferences); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}

863
server/channels/api4/preference_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,863 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGetPreferences(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
// recreate basic user (cached has no default preferences)
th.BasicUser = th.CreateUser()
th.LoginBasic()
user1 := th.BasicUser
category := model.NewId()
preferences1 := model.Preferences{
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: model.NewId(),
Name: model.NewId(),
},
}
client.UpdatePreferences(user1.Id, preferences1)
prefs, _, err := client.GetPreferences(user1.Id)
require.NoError(t, err)
// 6 because we have 3 initial preferences insights, tutorial_step and recommended_next_steps added when creating a new user
require.Equal(t, len(prefs), 6, "received the wrong number of preferences")
for _, preference := range prefs {
require.Equal(t, preference.UserId, th.BasicUser.Id, "user id does not match")
}
// recreate basic user2
th.BasicUser2 = th.CreateUser()
th.LoginBasic2()
prefs, _, err = client.GetPreferences(th.BasicUser2.Id)
require.NoError(t, err)
require.Greater(t, len(prefs), 0, "received the wrong number of preferences")
_, resp, err := client.GetPreferences(th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
_, resp, err = client.GetPreferences(th.BasicUser2.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetPreferencesByCategory(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
user1 := th.BasicUser
category := model.NewId()
preferences1 := model.Preferences{
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: model.NewId(),
Name: model.NewId(),
},
}
client.UpdatePreferences(user1.Id, preferences1)
prefs, _, err := client.GetPreferencesByCategory(user1.Id, category)
require.NoError(t, err)
require.Equal(t, len(prefs), 2, "received the wrong number of preferences")
_, resp, err := client.GetPreferencesByCategory(user1.Id, "junk")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
th.LoginBasic2()
_, resp, err = client.GetPreferencesByCategory(th.BasicUser2.Id, category)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, resp, err = client.GetPreferencesByCategory(user1.Id, category)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
prefs, resp, err = client.GetPreferencesByCategory(th.BasicUser2.Id, "junk")
require.Error(t, err)
CheckNotFoundStatus(t, resp)
require.Equal(t, len(prefs), 0, "received the wrong number of preferences")
client.Logout()
_, resp, err = client.GetPreferencesByCategory(th.BasicUser2.Id, category)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestGetPreferenceByCategoryAndName(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
user := th.BasicUser
name := model.NewId()
value := model.NewId()
preferences := model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: name,
Value: value,
},
{
UserId: user.Id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
Value: model.NewId(),
},
}
client.UpdatePreferences(user.Id, preferences)
pref, _, err := client.GetPreferenceByCategoryAndName(user.Id, model.PreferenceCategoryDirectChannelShow, name)
require.NoError(t, err)
require.Equal(t, preferences[0].UserId, pref.UserId, "UserId preference not saved")
require.Equal(t, preferences[0].Category, pref.Category, "Category preference not saved")
require.Equal(t, preferences[0].Name, pref.Name, "Name preference not saved")
preferences[0].Value = model.NewId()
client.UpdatePreferences(user.Id, preferences)
_, resp, err := client.GetPreferenceByCategoryAndName(user.Id, "junk", preferences[0].Name)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetPreferenceByCategoryAndName(user.Id, preferences[0].Category, "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetPreferenceByCategoryAndName(th.BasicUser2.Id, preferences[0].Category, "junk")
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, _, err = client.GetPreferenceByCategoryAndName(user.Id, preferences[0].Category, preferences[0].Name)
require.NoError(t, err)
client.Logout()
_, resp, err = client.GetPreferenceByCategoryAndName(user.Id, preferences[0].Category, preferences[0].Name)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestUpdatePreferences(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
user1 := th.BasicUser
category := model.NewId()
preferences1 := model.Preferences{
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: category,
Name: model.NewId(),
},
{
UserId: user1.Id,
Category: model.NewId(),
Name: model.NewId(),
},
}
_, err := client.UpdatePreferences(user1.Id, preferences1)
require.NoError(t, err)
preferences := model.Preferences{
{
UserId: model.NewId(),
Category: category,
Name: model.NewId(),
},
}
resp, err := client.UpdatePreferences(user1.Id, preferences)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
preferences = model.Preferences{
{
UserId: user1.Id,
Name: model.NewId(),
},
}
resp, err = client.UpdatePreferences(user1.Id, preferences)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UpdatePreferences(th.BasicUser2.Id, preferences)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
resp, err = client.UpdatePreferences(user1.Id, preferences1)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestUpdatePreferencesWebsocket(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
WebSocketClient, err := th.CreateWebSocketClient()
require.NoError(t, err)
WebSocketClient.Listen()
time.Sleep(300 * time.Millisecond)
wsResp := <-WebSocketClient.ResponseChannel
require.Equal(t, wsResp.Status, model.StatusOk, "expected OK from auth challenge")
userId := th.BasicUser.Id
preferences := model.Preferences{
{
UserId: userId,
Category: model.NewId(),
Name: model.NewId(),
},
{
UserId: userId,
Category: model.NewId(),
Name: model.NewId(),
},
}
_, err = th.Client.UpdatePreferences(userId, preferences)
require.NoError(t, err)
timeout := time.After(300 * time.Millisecond)
waiting := true
for waiting {
select {
case event := <-WebSocketClient.EventChannel:
if event.EventType() != model.WebsocketEventPreferencesChanged {
// Ignore any other events
continue
}
var received model.Preferences
jsonErr := json.Unmarshal([]byte(event.GetData()["preferences"].(string)), &received)
require.NoError(t, jsonErr)
for i, p := range preferences {
require.Equal(t, received[i].UserId, p.UserId, "received incorrect UserId")
require.Equal(t, received[i].Category, p.Category, "received incorrect Category")
require.Equal(t, received[i].Name, p.Name, "received incorrect Name")
}
waiting = false
case <-timeout:
require.Fail(t, "timed timed out waiting for preference update event")
}
}
}
func TestUpdateSidebarPreferences(t *testing.T) {
t.Run("when favoriting a channel, should add it to the Favorites sidebar category", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
user := th.BasicUser
team1 := th.CreateTeam()
th.LinkUserToTeam(user, team1)
_, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
channel := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, team1.Id)
th.AddUserToChannel(user, channel)
// Confirm that the sidebar is populated correctly to begin with
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
require.Contains(t, categories.Categories[1].Channels, channel.Id)
// Favorite the channel
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
},
})
require.NoError(t, err)
// Confirm that the channel was added to the Favorites
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.NotContains(t, categories.Categories[1].Channels, channel.Id)
// And unfavorite the channel
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "false",
},
})
require.NoError(t, err)
// The channel should've been removed from the Favorites
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.Contains(t, categories.Categories[1].Channels, channel.Id)
})
t.Run("when favoriting a DM channel, should add it to the Favorites sidebar category for all teams", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
user := th.BasicUser
user2 := th.BasicUser2
team1 := th.CreateTeam()
th.LinkUserToTeam(user, team1)
team2 := th.CreateTeam()
th.LinkUserToTeam(user, team2)
dmChannel := th.CreateDmChannel(user2)
// Favorite the channel
_, err := th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: dmChannel.Id,
Value: "true",
},
})
require.NoError(t, err)
// Confirm that the channel was added to the Favorites on all teams
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.NotContains(t, categories.Categories[2].Channels, dmChannel.Id)
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team2.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.NotContains(t, categories.Categories[2].Channels, dmChannel.Id)
// And unfavorite the channel
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: dmChannel.Id,
Value: "false",
},
})
require.NoError(t, err)
// The channel should've been removed from the Favorites on all teams
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.Contains(t, categories.Categories[2].Channels, dmChannel.Id)
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team2.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.Contains(t, categories.Categories[2].Channels, dmChannel.Id)
})
t.Run("when favoriting a channel, should not affect other users' favorites categories", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
user := th.BasicUser
user2 := th.BasicUser2
client2 := th.CreateClient()
th.LoginBasic2WithClient(client2)
team1 := th.CreateTeam()
th.LinkUserToTeam(user, team1)
th.LinkUserToTeam(user2, team1)
_, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
_, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
channel := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, team1.Id)
th.AddUserToChannel(user, channel)
th.AddUserToChannel(user2, channel)
// Confirm that the sidebar is populated correctly to begin with
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
require.Contains(t, categories.Categories[1].Channels, channel.Id)
categories, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
require.Contains(t, categories.Categories[1].Channels, channel.Id)
// Favorite the channel
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
},
})
require.NoError(t, err)
// Confirm that the channel was not added to Favorites for the second user
categories, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.Contains(t, categories.Categories[1].Channels, channel.Id)
// Favorite the channel for the second user
_, err = client2.UpdatePreferences(user2.Id, model.Preferences{
{
UserId: user2.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
},
})
require.NoError(t, err)
// Confirm that the channel is now in the Favorites for the second user
categories, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.NotContains(t, categories.Categories[1].Channels, channel.Id)
// And unfavorite the channel
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "false",
},
})
require.NoError(t, err)
// The channel should still be in the second user's favorites
categories, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.NotContains(t, categories.Categories[1].Channels, channel.Id)
})
}
func TestDeletePreferences(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
th.LoginBasic()
prefs, _, _ := client.GetPreferences(th.BasicUser.Id)
originalCount := len(prefs)
// save 10 preferences
var preferences model.Preferences
for i := 0; i < 10; i++ {
preference := model.Preference{
UserId: th.BasicUser.Id,
Category: model.PreferenceCategoryDirectChannelShow,
Name: model.NewId(),
}
preferences = append(preferences, preference)
}
client.UpdatePreferences(th.BasicUser.Id, preferences)
// delete 10 preferences
th.LoginBasic2()
resp, err := client.DeletePreferences(th.BasicUser2.Id, preferences)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.LoginBasic()
_, err = client.DeletePreferences(th.BasicUser.Id, preferences)
require.NoError(t, err)
resp, err = client.DeletePreferences(th.BasicUser2.Id, preferences)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
prefs, _, _ = client.GetPreferences(th.BasicUser.Id)
require.Len(t, prefs, originalCount, "should've deleted preferences")
client.Logout()
resp, err = client.DeletePreferences(th.BasicUser.Id, preferences)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestDeletePreferencesWebsocket(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
userId := th.BasicUser.Id
preferences := model.Preferences{
{
UserId: userId,
Category: model.NewId(),
Name: model.NewId(),
},
{
UserId: userId,
Category: model.NewId(),
Name: model.NewId(),
},
}
_, err := th.Client.UpdatePreferences(userId, preferences)
require.NoError(t, err)
WebSocketClient, err := th.CreateWebSocketClient()
require.NoError(t, err)
WebSocketClient.Listen()
wsResp := <-WebSocketClient.ResponseChannel
require.Equal(t, model.StatusOk, wsResp.Status, "should have responded OK to authentication challenge")
_, err = th.Client.DeletePreferences(userId, preferences)
require.NoError(t, err)
timeout := time.After(30000 * time.Millisecond)
waiting := true
for waiting {
select {
case event := <-WebSocketClient.EventChannel:
if event.EventType() != model.WebsocketEventPreferencesDeleted {
// Ignore any other events
continue
}
var received model.Preferences
jsonErr := json.Unmarshal([]byte(event.GetData()["preferences"].(string)), &received)
require.NoError(t, jsonErr)
for i, preference := range preferences {
require.Equal(t, preference.UserId, received[i].UserId)
require.Equal(t, preference.Category, received[i].Category)
require.Equal(t, preference.Name, received[i].Name)
}
waiting = false
case <-timeout:
require.Fail(t, "timed out waiting for preference delete event")
}
}
}
func TestDeleteSidebarPreferences(t *testing.T) {
t.Run("when removing a favorited channel preference, should remove it from the Favorites sidebar category", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
user := th.BasicUser
team1 := th.CreateTeam()
th.LinkUserToTeam(user, team1)
_, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
channel := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, team1.Id)
th.AddUserToChannel(user, channel)
// Confirm that the sidebar is populated correctly to begin with
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
require.Contains(t, categories.Categories[1].Channels, channel.Id)
// Favorite the channel
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
},
})
require.NoError(t, err)
// Confirm that the channel was added to the Favorites
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.NotContains(t, categories.Categories[1].Channels, channel.Id)
// And unfavorite the channel by deleting the preference
_, err = th.Client.DeletePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
},
})
require.NoError(t, err)
// The channel should've been removed from the Favorites
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.Contains(t, categories.Categories[1].Channels, channel.Id)
})
t.Run("when removing a favorited DM preference, should remove it from the Favorites sidebar category", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
user := th.BasicUser
user2 := th.BasicUser2
team1 := th.CreateTeam()
th.LinkUserToTeam(user, team1)
team2 := th.CreateTeam()
th.LinkUserToTeam(user, team2)
dmChannel := th.CreateDmChannel(user2)
// Favorite the channel
_, err := th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: dmChannel.Id,
Value: "true",
},
})
require.NoError(t, err)
// Confirm that the channel was added to the Favorites on all teams
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.NotContains(t, categories.Categories[2].Channels, dmChannel.Id)
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team2.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.NotContains(t, categories.Categories[2].Channels, dmChannel.Id)
// And unfavorite the channel by deleting the preference
_, err = th.Client.DeletePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: dmChannel.Id,
},
})
require.NoError(t, err)
// The channel should've been removed from the Favorites on all teams
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.Contains(t, categories.Categories[2].Channels, dmChannel.Id)
categories, _, err = th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team2.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, dmChannel.Id)
require.Equal(t, model.SidebarCategoryDirectMessages, categories.Categories[2].Type)
assert.Contains(t, categories.Categories[2].Channels, dmChannel.Id)
})
t.Run("when removing a favorited channel preference, should not affect other users' favorites categories", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
user := th.BasicUser
user2 := th.BasicUser2
client2 := th.CreateClient()
th.LoginBasic2WithClient(client2)
team1 := th.CreateTeam()
th.LinkUserToTeam(user, team1)
th.LinkUserToTeam(user2, team1)
_, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
_, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
channel := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, team1.Id)
th.AddUserToChannel(user, channel)
th.AddUserToChannel(user2, channel)
// Confirm that the sidebar is populated correctly to begin with
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(user.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
require.Contains(t, categories.Categories[1].Channels, channel.Id)
categories, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
require.NotContains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
require.Contains(t, categories.Categories[1].Channels, channel.Id)
// Favorite the channel for both users
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
},
})
require.NoError(t, err)
_, err = client2.UpdatePreferences(user2.Id, model.Preferences{
{
UserId: user2.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "true",
},
})
require.NoError(t, err)
// Confirm that the channel is in the Favorites for the second user
categories, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.NotContains(t, categories.Categories[1].Channels, channel.Id)
// And unfavorite the channel for the first user by deleting the preference
_, err = th.Client.UpdatePreferences(user.Id, model.Preferences{
{
UserId: user.Id,
Category: model.PreferenceCategoryFavoriteChannel,
Name: channel.Id,
Value: "false",
},
})
require.NoError(t, err)
// The channel should still be in the second user's favorites
categories, _, err = client2.GetSidebarCategoriesForTeamForUser(user2.Id, team1.Id, "")
require.NoError(t, err)
require.Equal(t, model.SidebarCategoryFavorites, categories.Categories[0].Type)
assert.Contains(t, categories.Categories[0].Channels, channel.Id)
require.Equal(t, model.SidebarCategoryChannels, categories.Categories[1].Type)
assert.NotContains(t, categories.Categories[1].Channels, channel.Id)
})
}

141
server/channels/api4/reaction.go Обычный файл
Просмотреть файл

@@ -0,0 +1,141 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitReaction() {
api.BaseRoutes.Reactions.Handle("", api.APISessionRequired(saveReaction)).Methods("POST")
api.BaseRoutes.Post.Handle("/reactions", api.APISessionRequired(getReactions)).Methods("GET")
api.BaseRoutes.ReactionByNameForPostForUser.Handle("", api.APISessionRequired(deleteReaction)).Methods("DELETE")
api.BaseRoutes.Posts.Handle("/ids/reactions", api.APISessionRequired(getBulkReactions)).Methods("POST")
}
func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
var reaction model.Reaction
if jsonErr := json.NewDecoder(r.Body).Decode(&reaction); jsonErr != nil {
c.SetInvalidParamWithErr("reaction", jsonErr)
return
}
if !model.IsValidId(reaction.UserId) || !model.IsValidId(reaction.PostId) || reaction.EmojiName == "" || len(reaction.EmojiName) > model.EmojiNameMaxLength {
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.invalid.app_error", nil, "", http.StatusBadRequest)
return
}
if reaction.UserId != c.AppContext.Session().UserId {
c.Err = model.NewAppError("saveReaction", "api.reaction.save_reaction.user_id.app_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), reaction.PostId, model.PermissionAddReaction) {
c.SetPermissionError(model.PermissionAddReaction)
return
}
re, err := c.App.SaveReactionForPost(c.AppContext, &reaction)
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(re); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getReactions(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
reactions, appErr := c.App.GetReactionsForPost(c.Params.PostId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(reactions)
if err != nil {
c.Err = model.NewAppError("getReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
c.RequirePostId()
if c.Err != nil {
return
}
c.RequireEmojiName()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionRemoveReaction) {
c.SetPermissionError(model.PermissionRemoveReaction)
return
}
if c.Params.UserId != c.AppContext.Session().UserId && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionRemoveOthersReactions) {
c.SetPermissionError(model.PermissionRemoveOthersReactions)
return
}
reaction := &model.Reaction{
UserId: c.Params.UserId,
PostId: c.Params.PostId,
EmojiName: c.Params.EmojiName,
}
err := c.App.DeleteReactionForPost(c.AppContext, reaction)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
postIds := model.ArrayFromJSON(r.Body)
for _, postId := range postIds {
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), postId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
}
reactions, appErr := c.App.GetBulkReactionsForPosts(postIds)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(reactions)
if err != nil {
c.Err = model.NewAppError("getBulkReactions", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}

586
server/channels/api4/reaction_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,586 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestSaveReaction(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
userId := th.BasicUser.Id
postId := th.BasicPost.Id
// Check the appropriate permissions are enforced.
defaultRolePermissions := th.SaveDefaultRolePermissions()
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
}()
reaction := &model.Reaction{
UserId: userId,
PostId: postId,
EmojiName: "smile",
}
t.Run("successful-reaction", func(t *testing.T) {
rr, _, err := client.SaveReaction(reaction)
require.NoError(t, err)
require.Equal(t, reaction.UserId, rr.UserId, "UserId did not match")
require.Equal(t, reaction.PostId, rr.PostId, "PostId did not match")
require.Equal(t, reaction.EmojiName, rr.EmojiName, "EmojiName did not match")
require.NotEqual(t, 0, rr.CreateAt, "CreateAt should exist")
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "didn't save reaction correctly")
})
t.Run("duplicated-reaction", func(t *testing.T) {
_, _, err := client.SaveReaction(reaction)
require.NoError(t, err)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "should have not save duplicated reaction")
})
t.Run("save-second-reaction", func(t *testing.T) {
reaction.EmojiName = "sad"
rr, _, err := client.SaveReaction(reaction)
require.NoError(t, err)
require.Equal(t, rr.EmojiName, reaction.EmojiName, "EmojiName did not match")
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr, "error saving multiple reactions")
require.Equal(t, len(reactions), 2, "should have save multiple reactions")
})
t.Run("saving-special-case", func(t *testing.T) {
reaction.EmojiName = "+1"
rr, _, err := client.SaveReaction(reaction)
require.NoError(t, err)
require.Equal(t, reaction.EmojiName, rr.EmojiName, "EmojiName did not match")
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 3, len(reactions), "should have save multiple reactions")
})
t.Run("react-to-not-existing-post-id", func(t *testing.T) {
reaction.PostId = GenerateTestId()
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("react-to-not-valid-post-id", func(t *testing.T) {
reaction.PostId = "junk"
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("react-as-not-existing-user-id", func(t *testing.T) {
reaction.PostId = postId
reaction.UserId = GenerateTestId()
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("react-as-not-valid-user-id", func(t *testing.T) {
reaction.UserId = "junk"
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("react-as-empty-emoji-name", func(t *testing.T) {
reaction.UserId = userId
reaction.EmojiName = ""
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("react-as-not-valid-emoji-name", func(t *testing.T) {
reaction.EmojiName = strings.Repeat("a", 65)
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("react-as-other-user", func(t *testing.T) {
reaction.EmojiName = "smile"
otherUser := th.CreateUser()
client.Logout()
client.Login(otherUser.Email, otherUser.Password)
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("react-being-not-logged-in", func(t *testing.T) {
client.Logout()
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("react-as-other-user-being-system-admin", func(t *testing.T) {
_, resp, err := th.SystemAdminClient.SaveReaction(reaction)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("unable-to-create-reaction-without-permissions", func(t *testing.T) {
th.LoginBasic()
th.RemovePermissionFromRole(model.PermissionAddReaction.Id, model.ChannelUserRoleId)
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 3, len(reactions), "should have not created a reactions")
th.AddPermissionToRole(model.PermissionAddReaction.Id, model.ChannelUserRoleId)
})
t.Run("unable-to-react-in-an-archived-channel", func(t *testing.T) {
th.LoginBasic()
channel := th.CreatePublicChannel()
post := th.CreatePostWithClient(th.Client, channel)
reaction := &model.Reaction{
UserId: userId,
PostId: post.Id,
EmojiName: "smile",
}
appErr := th.App.DeleteChannel(th.Context, channel, userId)
assert.Nil(t, appErr)
_, resp, err := client.SaveReaction(reaction)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
reactions, appErr := th.App.GetReactionsForPost(post.Id)
require.Nil(t, appErr)
require.Equal(t, 0, len(reactions), "should have not created a reaction")
})
}
func TestGetReactions(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
userId := th.BasicUser.Id
user2Id := th.BasicUser2.Id
postId := th.BasicPost.Id
userReactions := []*model.Reaction{
{
UserId: userId,
PostId: postId,
EmojiName: "smile",
},
{
UserId: userId,
PostId: postId,
EmojiName: "happy",
},
{
UserId: userId,
PostId: postId,
EmojiName: "sad",
},
{
UserId: user2Id,
PostId: postId,
EmojiName: "smile",
},
{
UserId: user2Id,
PostId: postId,
EmojiName: "sad",
},
}
var reactions []*model.Reaction
for _, userReaction := range userReactions {
reaction, err := th.App.Srv().Store().Reaction().Save(userReaction)
require.NoError(t, err)
reactions = append(reactions, reaction)
}
t.Run("get-reactions", func(t *testing.T) {
rr, _, err := client.GetReactions(postId)
require.NoError(t, err)
assert.Len(t, rr, 5)
for _, r := range reactions {
assert.Contains(t, reactions, r)
}
})
t.Run("get-reactions-of-invalid-post-id", func(t *testing.T) {
rr, resp, err := client.GetReactions("junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
assert.Empty(t, rr)
})
t.Run("get-reactions-of-not-existing-post-id", func(t *testing.T) {
_, resp, err := client.GetReactions(GenerateTestId())
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("get-reactions-as-anonymous-user", func(t *testing.T) {
client.Logout()
_, resp, err := client.GetReactions(postId)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("get-reactions-as-system-admin", func(t *testing.T) {
_, _, err := th.SystemAdminClient.GetReactions(postId)
require.NoError(t, err)
})
}
func TestDeleteReaction(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
userId := th.BasicUser.Id
user2Id := th.BasicUser2.Id
postId := th.BasicPost.Id
r1 := &model.Reaction{
UserId: userId,
PostId: postId,
EmojiName: "smile",
}
r2 := &model.Reaction{
UserId: userId,
PostId: postId,
EmojiName: "smile-",
}
r3 := &model.Reaction{
UserId: userId,
PostId: postId,
EmojiName: "+1",
}
r4 := &model.Reaction{
UserId: user2Id,
PostId: postId,
EmojiName: "smile_",
}
// Check the appropriate permissions are enforced.
defaultRolePermissions := th.SaveDefaultRolePermissions()
defer func() {
th.RestoreDefaultRolePermissions(defaultRolePermissions)
}()
t.Run("delete-reaction", func(t *testing.T) {
th.App.SaveReactionForPost(th.Context, r1)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "didn't save reaction correctly")
_, err := client.DeleteReaction(r1)
require.NoError(t, err)
reactions, appErr = th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 0, len(reactions), "should have deleted reaction")
})
t.Run("delete-reaction-when-post-has-multiple-reactions", func(t *testing.T) {
th.App.SaveReactionForPost(th.Context, r1)
th.App.SaveReactionForPost(th.Context, r2)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, len(reactions), 2, "didn't save reactions correctly")
_, err := client.DeleteReaction(r2)
require.NoError(t, err)
reactions, appErr = th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "should have deleted only 1 reaction")
require.Equal(t, *r1, *reactions[0], "should have deleted 1 reaction only")
})
t.Run("delete-reaction-when-plus-one-reaction-name", func(t *testing.T) {
th.App.SaveReactionForPost(th.Context, r3)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 2, len(reactions), "didn't save reactions correctly")
_, err := client.DeleteReaction(r3)
require.NoError(t, err)
reactions, appErr = th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "should have deleted 1 reaction only")
require.Equal(t, *r1, *reactions[0], "should have deleted 1 reaction only")
})
t.Run("delete-reaction-made-by-another-user", func(t *testing.T) {
th.LoginBasic2()
th.App.SaveReactionForPost(th.Context, r4)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 2, len(reactions), "didn't save reaction correctly")
th.LoginBasic()
resp, err := client.DeleteReaction(r4)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
reactions, appErr = th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 2, len(reactions), "should have not deleted a reaction")
})
t.Run("delete-reaction-from-not-existing-post-id", func(t *testing.T) {
r1.PostId = GenerateTestId()
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("delete-reaction-from-not-valid-post-id", func(t *testing.T) {
r1.PostId = "junk"
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("delete-reaction-from-not-existing-user-id", func(t *testing.T) {
r1.PostId = postId
r1.UserId = GenerateTestId()
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("delete-reaction-from-not-valid-user-id", func(t *testing.T) {
r1.UserId = "junk"
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("delete-reaction-with-empty-name", func(t *testing.T) {
r1.UserId = userId
r1.EmojiName = ""
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckNotFoundStatus(t, resp)
})
t.Run("delete-reaction-with-not-existing-name", func(t *testing.T) {
r1.EmojiName = strings.Repeat("a", 65)
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("delete-reaction-as-anonymous-user", func(t *testing.T) {
client.Logout()
r1.EmojiName = "smile"
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("delete-reaction-as-system-admin", func(t *testing.T) {
_, err := th.SystemAdminClient.DeleteReaction(r1)
require.NoError(t, err)
_, err = th.SystemAdminClient.DeleteReaction(r4)
require.NoError(t, err)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 0, len(reactions), "should have deleted both reactions")
})
t.Run("unable-to-delete-reaction-without-permissions", func(t *testing.T) {
th.LoginBasic()
th.RemovePermissionFromRole(model.PermissionRemoveReaction.Id, model.ChannelUserRoleId)
th.App.SaveReactionForPost(th.Context, r1)
resp, err := client.DeleteReaction(r1)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "should have not deleted a reactions")
th.AddPermissionToRole(model.PermissionRemoveReaction.Id, model.ChannelUserRoleId)
})
t.Run("unable-to-delete-others-reactions-without-permissions", func(t *testing.T) {
th.RemovePermissionFromRole(model.PermissionRemoveOthersReactions.Id, model.SystemAdminRoleId)
th.App.SaveReactionForPost(th.Context, r1)
resp, err := th.SystemAdminClient.DeleteReaction(r1)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "should have not deleted a reactions")
th.AddPermissionToRole(model.PermissionRemoveOthersReactions.Id, model.SystemAdminRoleId)
})
t.Run("unable-to-delete-reactions-in-an-archived-channel", func(t *testing.T) {
th.LoginBasic()
channel := th.CreatePublicChannel()
post := th.CreatePostWithClient(th.Client, channel)
reaction := &model.Reaction{
UserId: userId,
PostId: post.Id,
EmojiName: "smile",
}
r1, _, err := client.SaveReaction(reaction)
require.NoError(t, err)
reactions, appErr := th.App.GetReactionsForPost(postId)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "should have created a reaction")
appErr = th.App.DeleteChannel(th.Context, channel, userId)
assert.Nil(t, appErr)
_, resp, err := client.SaveReaction(r1)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
reactions, appErr = th.App.GetReactionsForPost(post.Id)
require.Nil(t, appErr)
require.Equal(t, 1, len(reactions), "should have not deleted a reaction")
})
}
func TestGetBulkReactions(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
client := th.Client
userId := th.BasicUser.Id
user2Id := th.BasicUser2.Id
post1 := &model.Post{UserId: userId, ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a"}
post2 := &model.Post{UserId: userId, ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a"}
post3 := &model.Post{UserId: userId, ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a"}
post4 := &model.Post{UserId: user2Id, ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a"}
post5 := &model.Post{UserId: user2Id, ChannelId: th.BasicChannel.Id, Message: "zz" + model.NewId() + "a"}
post1, _, _ = client.CreatePost(post1)
post2, _, _ = client.CreatePost(post2)
post3, _, _ = client.CreatePost(post3)
post4, _, _ = client.CreatePost(post4)
post5, _, _ = client.CreatePost(post5)
expectedPostIdsReactionsMap := make(map[string][]*model.Reaction)
expectedPostIdsReactionsMap[post1.Id] = []*model.Reaction{}
expectedPostIdsReactionsMap[post2.Id] = []*model.Reaction{}
expectedPostIdsReactionsMap[post3.Id] = []*model.Reaction{}
expectedPostIdsReactionsMap[post5.Id] = []*model.Reaction{}
userReactions := []*model.Reaction{
{
UserId: userId,
PostId: post1.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "smile",
},
{
UserId: user2Id,
PostId: post4.Id,
EmojiName: "smile",
},
}
for _, userReaction := range userReactions {
reactions := expectedPostIdsReactionsMap[userReaction.PostId]
reaction, err := th.App.Srv().Store().Reaction().Save(userReaction)
require.NoError(t, err)
reactions = append(reactions, reaction)
expectedPostIdsReactionsMap[userReaction.PostId] = reactions
}
postIds := []string{post1.Id, post2.Id, post3.Id, post4.Id, post5.Id}
t.Run("get-reactions", func(t *testing.T) {
postIdsReactionsMap, _, err := client.GetBulkReactions(postIds)
require.NoError(t, err)
assert.ElementsMatch(t, expectedPostIdsReactionsMap[post1.Id], postIdsReactionsMap[post1.Id])
assert.ElementsMatch(t, expectedPostIdsReactionsMap[post2.Id], postIdsReactionsMap[post2.Id])
assert.ElementsMatch(t, expectedPostIdsReactionsMap[post3.Id], postIdsReactionsMap[post3.Id])
assert.ElementsMatch(t, expectedPostIdsReactionsMap[post4.Id], postIdsReactionsMap[post4.Id])
assert.ElementsMatch(t, expectedPostIdsReactionsMap[post5.Id], postIdsReactionsMap[post5.Id])
assert.Equal(t, expectedPostIdsReactionsMap, postIdsReactionsMap)
})
t.Run("get-reactions-as-anonymous-user", func(t *testing.T) {
client.Logout()
_, resp, err := client.GetBulkReactions(postIds)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
}

287
server/channels/api4/remote_cluster.go Обычный файл
Просмотреть файл

@@ -0,0 +1,287 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"io"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (api *API) InitRemoteCluster() {
api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData)).Methods("POST")
api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage)).Methods("POST")
}
func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is enabled.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterPing", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
var ping model.RemoteClusterPing
if err := json.Unmarshal(frame.Msg.Payload, &ping); err != nil {
c.SetInvalidParamWithErr("msg.payload", err)
return
}
ping.RecvAt = model.GetMillis()
if metrics := c.App.Metrics(); metrics != nil {
metrics.IncrementRemoteClusterMsgReceivedCounter(rc.RemoteId)
}
err := json.NewEncoder(w).Encode(ping)
if err != nil {
c.Logger.Warn("Error writing response", mlog.Err(err))
}
}
func remoteClusterAcceptMessage(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
service, appErr := c.App.GetRemoteClusterService()
if appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if err := json.NewDecoder(r.Body).Decode(&frame); err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
appErr = frame.IsValid()
if appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptMessage", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, appErr := c.App.GetRemoteCluster(frame.RemoteId)
if appErr != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
// pass message to Remote Cluster Service and write response
resp := service.ReceiveIncomingMsg(rc, frame.Msg)
b, err := json.Marshal(resp)
if err != nil {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func remoteClusterConfirmInvite(c *Context, w http.ResponseWriter, r *http.Request) {
// make sure remote cluster service is running.
if _, appErr := c.App.GetRemoteClusterService(); appErr != nil {
c.Err = appErr
return
}
var frame model.RemoteClusterFrame
if jsonErr := json.NewDecoder(r.Body).Decode(&frame); jsonErr != nil {
c.Err = model.NewAppError("remoteClusterConfirmInvite", "api.unmarshal_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
return
}
if appErr := frame.IsValid(); appErr != nil {
c.Err = appErr
return
}
auditRec := c.MakeAuditRecord("remoteClusterAcceptInvite", audit.Fail)
audit.AddEventParameterAuditable(auditRec, "remote_cluster_frame", &frame)
defer c.LogAuditRec(auditRec)
remoteId := c.GetRemoteID(r)
if remoteId != frame.RemoteId {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
rc, err := c.App.GetRemoteCluster(frame.RemoteId)
if err != nil {
c.SetInvalidRemoteIdError(frame.RemoteId)
return
}
audit.AddEventParameterAuditable(auditRec, "remote_cluster", rc)
if time.Since(model.GetTimeForMillis(rc.CreateAt)) > remotecluster.InviteExpiresAfter {
c.Err = model.NewAppError("remoteClusterAcceptMessage", "api.context.invitation_expired.error", nil, "", http.StatusBadRequest)
return
}
var confirm model.RemoteClusterInvite
if jsonErr := json.Unmarshal(frame.Msg.Payload, &confirm); jsonErr != nil {
c.SetInvalidParam("msg.payload")
return
}
rc.RemoteTeamId = confirm.RemoteTeamId
rc.SiteURL = confirm.SiteURL
rc.RemoteToken = confirm.Token
if _, err := c.App.UpdateRemoteCluster(rc); err != nil {
c.Err = err
return
}
auditRec.Success()
ReturnStatusOK(w)
}
func uploadRemoteData(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadRemoteData", "api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
c.RequireUploadId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord("uploadRemoteData", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "upload_id", c.Params.UploadId)
c.AppContext.SetContext(app.WithMaster(c.AppContext.Context()))
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.RemoteId != c.GetRemoteID(r) {
c.Err = model.NewAppError("uploadRemoteData", "api.context.remote_id_mismatch.app_error",
nil, "", http.StatusUnauthorized)
return
}
info, err := doUploadData(c, us, r)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if info == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func remoteSetProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
defer io.Copy(io.Discard, r.Body)
c.RequireUserId()
if c.Err != nil {
return
}
if *c.App.Config().FileSettings.DriverName == "" {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.storage.app_error", nil, "", http.StatusNotImplemented)
return
}
if r.ContentLength > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge)
return
}
if err := r.ParseMultipartForm(*c.App.Config().FileSettings.MaxFileSize); err != nil {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.parse.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
m := r.MultipartForm
imageArray, ok := m.File["image"]
if !ok {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.no_file.app_error", nil, "", http.StatusBadRequest)
return
}
if len(imageArray) == 0 {
c.Err = model.NewAppError("remoteUploadProfileImage", "api.user.upload_profile_user.array.app_error", nil, "", http.StatusBadRequest)
return
}
auditRec := c.MakeAuditRecord("remoteUploadProfileImage", audit.Fail)
defer c.LogAuditRec(auditRec)
if imageArray[0] != nil {
audit.AddEventParameter(auditRec, "filename", imageArray[0].Filename)
}
user, err := c.App.GetUser(c.Params.UserId)
if err != nil || !user.IsRemote() {
c.SetInvalidURLParam("user_id")
return
}
audit.AddEventParameterAuditable(auditRec, "user", user)
imageData := imageArray[0]
if err := c.App.SetProfileImage(c.AppContext, c.Params.UserId, imageData); err != nil {
c.Err = err
return
}
auditRec.Success()
c.LogAudit("")
ReturnStatusOK(w)
}

427
server/channels/api4/resolver.go Обычный файл
Просмотреть файл

@@ -0,0 +1,427 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"errors"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// cursorPrefix is used to categorize objects
// sent in a cursor. The type is prepended
// to the string with a - to find which
// object the id belongs to.
//
// And after the type is extracted, object
// specific logic can be applied to extract the id.
type cursorPrefix string
const (
channelMemberCursorPrefix cursorPrefix = "channelMember"
channelCursorPrefix cursorPrefix = "channel"
)
type resolver struct {
}
// match with api4.getChannelsForTeamForUser
func (r *resolver) Channels(ctx context.Context, args struct {
TeamID string
UserID string
IncludeDeleted bool
LastDeleteAt float64
LastUpdateAt float64
First int32
After string
}) ([]*channel, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
if args.TeamID != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel string
var ok bool
if args.After != "" {
afterChannel, ok = parseChannelCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
// TODO: convert this to a streaming API.
channels, appErr := c.App.GetChannelsForTeamForUserWithCursor(c.AppContext, args.TeamID, args.UserID, &model.ChannelSearchOpts{
IncludeDeleted: args.IncludeDeleted,
LastDeleteAt: int(args.LastDeleteAt),
LastUpdateAt: int(args.LastUpdateAt),
PerPage: model.NewInt(limit),
}, afterChannel)
if appErr != nil {
return nil, appErr
}
appErr = c.App.FillInChannelsProps(c.AppContext, channels)
if appErr != nil {
return nil, appErr
}
return postProcessChannels(c, channels)
}
// match with api4.getUser
func (r *resolver) User(ctx context.Context, args struct{ ID string }) (*user, error) {
return getGraphQLUser(ctx, args.ID)
}
// match with api4.getClientConfig
func (r *resolver) Config(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.AppContext.Session().UserId == "" {
return c.App.Srv().Platform().LimitedClientConfigWithComputed(), nil
}
return c.App.Srv().Platform().ClientConfigWithComputed(), nil
}
// match with api4.getClientLicense
func (r *resolver) License(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
return c.App.Srv().ClientLicense(), nil
}
return c.App.Srv().GetSanitizedClientLicense(), nil
}
// match with api4.getTeamMembersForUser for teamID=""
// and api4.getTeamMember for teamID != ""
func (r *resolver) TeamMembers(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*teamMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOtherUsersTeams) {
c.SetPermissionError(model.PermissionReadOtherUsersTeams)
return nil, c.Err
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, args.UserID)
if appErr != nil {
return nil, appErr
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return nil, c.Err
}
if args.TeamID != "" && !args.ExcludeTeam {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
tm, appErr2 := c.App.GetTeamMember(args.TeamID, args.UserID)
if appErr2 != nil {
return nil, appErr2
}
return []*teamMember{{*tm}}, nil
}
excludeTeamID := ""
if args.TeamID != "" && args.ExcludeTeam {
excludeTeamID = args.TeamID
}
// Do not return archived team members
members, appErr := c.App.GetTeamMembersForUser(args.UserID, excludeTeamID, false)
if appErr != nil {
return nil, appErr
}
// Convert to the wrapper format.
res := make([]*teamMember, 0, len(members))
for _, tm := range members {
res = append(res, &teamMember{*tm})
}
return res, nil
}
func (*resolver) ChannelsLeft(ctx context.Context, args struct {
UserID string
Since float64
}) ([]string, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
return c.App.Srv().Store().ChannelMemberHistory().GetChannelsLeftSince(args.UserID, int64(args.Since))
}
// match with api4.getChannelMember
func (*resolver) ChannelMembers(ctx context.Context, args struct {
UserID string
TeamID string
ChannelID string
ExcludeTeam bool
First int32
After string
LastUpdateAt float64
}) ([]*channelMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
// If it's a single channel
if args.ChannelID != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), args.ChannelID, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
ctx := c.AppContext
ctx.SetContext(app.WithMaster(ctx.Context()))
member, appErr := c.App.GetChannelMember(ctx, args.ChannelID, args.UserID)
if appErr != nil {
return nil, appErr
}
return []*channelMember{{*member}}, nil
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel, afterUser string
var ok bool
if args.After != "" {
afterChannel, afterUser, ok = parseChannelMemberCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
if args.TeamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*channelMember{}, appErr
}
args.TeamID = team.Id
} else {
return []*channelMember{}, nil
}
}
}
opts := &store.ChannelMemberGraphQLSearchOpts{
AfterChannel: afterChannel,
AfterUser: afterUser,
Limit: limit,
LastUpdateAt: int(args.LastUpdateAt),
ExcludeTeam: args.ExcludeTeam,
}
members, err := c.App.Srv().Store().Channel().GetMembersForUserWithCursor(args.UserID, args.TeamID, opts)
if err != nil {
return nil, err
}
res := make([]*channelMember, 0, len(members))
for _, cm := range members {
res = append(res, &channelMember{cm})
}
return res, nil
}
// match with api4.getCategoriesForTeamForUser
func (*resolver) SidebarCategories(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*model.SidebarCategoryWithChannels, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
// Fallback to primary team logic
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*model.SidebarCategoryWithChannels{}, appErr
}
args.TeamID = team.Id
} else {
return []*model.SidebarCategoryWithChannels{}, nil
}
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
// If it's only for a single team.
var categories *model.OrderedSidebarCategories
var appErr *model.AppError
if !args.ExcludeTeam {
categories, appErr = c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, args.UserID, args.TeamID)
if appErr != nil {
return nil, appErr
}
} else {
opts := &store.SidebarCategorySearchOpts{
TeamID: args.TeamID,
ExcludeTeam: args.ExcludeTeam,
}
categories, appErr = c.App.GetSidebarCategories(c.AppContext, args.UserID, opts)
if appErr != nil {
return nil, appErr
}
}
// TODO: look into optimizing this.
// create map
orderMap := make(map[string]*model.SidebarCategoryWithChannels, len(categories.Categories))
for _, category := range categories.Categories {
orderMap[category.Id] = category
}
// create a new slice based on the order
res := make([]*model.SidebarCategoryWithChannels, 0, len(categories.Categories))
for _, categoryId := range categories.Order {
res = append(res, orderMap[categoryId])
}
return res, nil
}
// getCtx extracts web.Context out of the usual request context.
// Kind of an anti-pattern, but there are lots of methods attached to *web.Context
// so we use it for now.
func getCtx(ctx context.Context) (*web.Context, error) {
c, ok := ctx.Value(webCtx).(*web.Context)
if !ok {
return nil, errors.New("no web.Context found in context")
}
return c, nil
}
// getRolesLoader returns the roles loader out of the context.
func getRolesLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(rolesLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getChannelsLoader returns the channels loader out of the context.
func getChannelsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(channelsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getTeamsLoader returns the teams loader out of the context.
func getTeamsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(teamsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getUsersLoader returns the users loader out of the context.
func getUsersLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(usersLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}

157
server/channels/api4/resolver_channel.go Обычный файл
Просмотреть файл

@@ -0,0 +1,157 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/base64"
"fmt"
"sort"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// channel is an internal graphQL wrapper struct to add resolver methods.
type channel struct {
model.Channel
PrettyDisplayName string
}
// match with api4.getTeam
func (ch *channel) Team(ctx context.Context) (*model.Team, error) {
if ch.TeamId == "" {
return nil, nil
}
return getGraphQLTeam(ctx, ch.TeamId)
}
func (ch *channel) Cursor() *string {
cursor := string(channelCursorPrefix) + "-" + ch.Id
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
return model.NewString(encoded)
}
func parseChannelCursor(cursor string) (channelID string, ok bool) {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil {
return "", false
}
prefix, id, found := strings.Cut(string(decoded), "-")
if !found {
return "", false
}
if cursorPrefix(prefix) != channelCursorPrefix {
return "", false
}
return id, true
}
func postProcessChannels(c *web.Context, channels []*model.Channel) ([]*channel, error) {
// This approach becomes effectively similar to a dataloader if the displayName computation
// were to be done at the field level per channel.
// Get DM/GM channelIDs and set empty maps as well.
var channelIDs []string
for _, ch := range channels {
if ch.IsGroupOrDirect() {
channelIDs = append(channelIDs, ch.Id)
}
// This is needed to avoid sending null, which
// does not match with the schema since props is not nullable.
// And making it nullable would mean taking pointer of a map,
// which is not very idiomatic.
ch.MakeNonNil()
}
var nameFormat string
var userInfo map[string][]*model.User
var err error
// Avoiding unnecessary queries unless necessary.
if len(channelIDs) > 0 {
userInfo, err = c.App.Srv().Store().Channel().GetMembersInfoByChannelIds(channelIDs)
if err != nil {
return nil, err
}
user := &model.User{Id: c.AppContext.Session().UserId}
nameFormat = c.App.GetNotificationNameFormat(user)
}
// Convert to the wrapper format.
nameCache := make(map[string]string)
res := make([]*channel, len(channels))
for i, ch := range channels {
prettyName := ch.DisplayName
if ch.IsGroupOrDirect() {
// get users slice for channel id
users := userInfo[ch.Id]
if users == nil {
return nil, fmt.Errorf("user info not found for channel id: %s", ch.Id)
}
prettyName = getPrettyDNForUsers(nameFormat, users, c.AppContext.Session().UserId, nameCache)
}
res[i] = &channel{Channel: *ch, PrettyDisplayName: prettyName}
}
return res, nil
}
func getPrettyDNForUsers(displaySetting string, users []*model.User, omitUserId string, cache map[string]string) string {
displayNames := make([]string, 0, len(users))
for _, u := range users {
if u.Id == omitUserId {
continue
}
displayNames = append(displayNames, getPrettyDNForUser(displaySetting, u, cache))
}
sort.Strings(displayNames)
result := strings.Join(displayNames, ", ")
if result == "" {
// Self DM
result = getPrettyDNForUser(displaySetting, users[0], cache)
}
return result
}
func getPrettyDNForUser(displaySetting string, user *model.User, cache map[string]string) string {
// use the cache first
if name, ok := cache[user.Id]; ok {
return name
}
var displayName string
switch displaySetting {
case "nickname_full_name":
displayName = user.Nickname
if strings.TrimSpace(displayName) == "" {
displayName = user.GetFullName()
}
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
case "full_name":
displayName = user.GetFullName()
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
default: // the "username" case also falls under this one.
displayName = user.Username
}
// update the cache
cache[user.Id] = displayName
return displayName
}

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

@@ -0,0 +1,226 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/base64"
"fmt"
"strings"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// channelMember is an internal graphQL wrapper struct to add resolver methods.
type channelMember struct {
model.ChannelMember
}
// match with api4.getUser
func (cm *channelMember) User(ctx context.Context) (*user, error) {
return getGraphQLUser(ctx, cm.UserId)
}
// match with api4.Channel
func (cm *channelMember) Channel(ctx context.Context) (*channel, error) {
loader, err := getChannelsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(cm.ChannelId))
result, err := thunk()
if err != nil {
return nil, err
}
channel := result.(*channel)
return channel, nil
}
func graphQLChannelsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
channels, err := getGraphQLChannels(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range channels {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLChannels(c *web.Context, channelIDs []string) ([]*channel, error) {
channels, appErr := c.App.GetChannels(c.AppContext, channelIDs)
if appErr != nil {
return nil, appErr
}
if len(channels) != len(channelIDs) {
return nil, fmt.Errorf("all channels were not found. Requested %d; Found %d", len(channelIDs), len(channels))
}
var openChannels, nonOpenChannels, teamsForOpenChannels []string
uniqueTeams := make(map[string]bool)
for _, ch := range channels {
if ch.Type == model.ChannelTypeOpen {
openChannels = append(openChannels, ch.Id)
uniqueTeams[ch.TeamId] = true
} else {
nonOpenChannels = append(nonOpenChannels, ch.Id)
}
}
for teamID := range uniqueTeams {
teamsForOpenChannels = append(teamsForOpenChannels, teamID)
}
if len(openChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), openChannels, model.PermissionReadChannel) &&
!c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsForOpenChannels, model.PermissionReadPublicChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return nil, c.Err
}
if len(nonOpenChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), nonOpenChannels, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
appErr = c.App.FillInChannelsProps(c.AppContext, model.ChannelList(channels))
if appErr != nil {
return nil, appErr
}
res, err := postProcessChannels(c, channels)
if err != nil {
return nil, err
}
// The channels need to be in the exact same order as the input slice.
tmp := make(map[string]*channel)
for _, ch := range res {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the channels.
for i, id := range channelIDs {
res[i] = tmp[id]
}
return res, nil
}
func (cm *channelMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(cm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
func (cm *channelMember) Cursor() *string {
cursor := string(channelMemberCursorPrefix) + "-" + cm.ChannelId + "-" + cm.UserId
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
return model.NewString(encoded)
}
func graphQLRolesLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
roles, err := getGraphQLRoles(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, role := range roles {
result[i] = &dataloader.Result{Data: role}
}
return result
}
func getGraphQLRoles(c *web.Context, roleNames []string) ([]*model.Role, error) {
cleanedRoleNames, valid := model.CleanRoleNames(roleNames)
if !valid {
c.SetInvalidParam("rolename")
return nil, c.Err
}
roles, appErr := c.App.GetRolesByNames(cleanedRoleNames)
if appErr != nil {
return nil, appErr
}
// The roles need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Role)
for _, r := range roles {
tmp[r.Name] = r
}
// We reuse the same slice and just rewrite the roles.
for i, roleName := range roleNames {
roles[i] = tmp[roleName]
}
return roles, nil
}
func parseChannelMemberCursor(cursor string) (channelID, userID string, ok bool) {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err != nil {
return "", "", false
}
parts := strings.Split(string(decoded), "-")
if len(parts) != 3 {
return "", "", false
}
if cursorPrefix(parts[0]) != channelMemberCursorPrefix {
return "", "", false
}
return parts[1], parts[2], true
}

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

@@ -0,0 +1,400 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGraphQLChannelMembers(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t).InitBasic()
defer th.TearDown()
// Adding another team with more channels (public and private)
myTeam := th.CreateTeam()
ch1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, myTeam.Id)
ch2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypePrivate, myTeam.Id)
th.LinkUserToTeam(th.BasicUser, myTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch2, false)
// Creating some msgcount
th.CreateMessagePostWithClient(th.Client, th.BasicChannel, "basic post")
th.CreateMessagePostWithClient(th.Client, ch1, "ch1 post")
var q struct {
ChannelMembers []struct {
Channel struct {
ID string `json:"id"`
CreateAt float64 `json:"createAt"`
UpdateAt float64 `json:"updateAt"`
Type model.ChannelType `json:"type"`
DisplayName string `json:"displayName"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
Team struct {
ID string `json:"id"`
} `json:"team"`
} `json:"channel"`
User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
NickName string `json:"nickname"`
} `json:"user"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
Permissions []string `json:"permissions"`
SchemeManaged bool `json:"schemeManaged"`
BuiltIn bool `json:"builtIn"`
} `json:"roles"`
LastViewedAt float64 `json:"lastViewedAt"`
LastUpdateAt float64 `json:"lastUpdateAt"`
MsgCount float64 `json:"msgCount"`
MentionCount float64 `json:"mentionCount"`
MentionCountRoot float64 `json:"mentionCountRoot"`
UrgentMentionCount float64 `json:"urgentMentionCount"`
MsgCountRoot float64 `json:"msgCountRoot"`
NotifyProps model.StringMap `json:"notifyProps"`
SchemeGuest bool `json:"schemeGuest"`
SchemeUser bool `json:"schemeUser"`
SchemeAdmin bool `json:"schemeAdmin"`
Cursor string `json:"cursor"`
} `json:"channelMembers"`
}
t.Run("all", func(t *testing.T) {
input := graphQLInput{
OperationName: "channelMembers",
Query: `
query channelMembers {
channelMembers(userId: "me") {
channel {
id
createAt
updateAt
type
displayName
name
header
team {
id
}
}
user {
id
username
email
}
msgCount
mentionCount
mentionCountRoot
urgentMentionCount
msgCountRoot
schemeGuest
schemeUser
schemeAdmin
cursor
}
}
`,
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelMembers, 9)
numPrivate := 0
numPublic := 0
numOffTopic := 0
numTownSquare := 0
for _, ch := range q.ChannelMembers {
assert.NotEmpty(t, ch.Channel.ID)
assert.NotEmpty(t, ch.Channel.Name)
assert.NotEmpty(t, ch.Channel.CreateAt)
assert.NotEmpty(t, ch.Channel.UpdateAt)
if ch.Channel.Type == model.ChannelTypeOpen {
numPublic++
} else if ch.Channel.Type == model.ChannelTypePrivate {
numPrivate++
}
if ch.Channel.DisplayName == "Off-Topic" {
numOffTopic++
} else if ch.Channel.DisplayName == "Town Square" {
numTownSquare++
}
assert.Equal(t, th.BasicUser.Id, ch.User.ID)
assert.Equal(t, th.BasicUser.Username, ch.User.Username)
assert.Equal(t, th.BasicUser.Email, ch.User.Email)
assert.False(t, ch.SchemeGuest)
if ch.Channel.Team.ID == myTeam.Id {
assert.True(t, ch.SchemeAdmin)
} else {
assert.False(t, ch.SchemeAdmin)
}
assert.True(t, ch.SchemeUser)
assert.NotEmpty(t, ch.Cursor)
switch ch.Channel.ID {
case th.BasicChannel.Id:
assert.Equal(t, float64(2), ch.MsgCount)
case ch1.Id:
assert.Equal(t, float64(1), ch.MsgCount)
}
}
assert.Equal(t, 2, numPrivate)
assert.Equal(t, 7, numPublic)
assert.Equal(t, 2, numOffTopic)
assert.Equal(t, 2, numTownSquare)
})
t.Run("user_perms", func(t *testing.T) {
input := graphQLInput{
OperationName: "channelMembers",
Query: `
query channelMembers($user: String!) {
channelMembers(userId: $user) {
channel {
id
createAt
updateAt
}
msgCount
mentionCount
mentionCountRoot
urgentMentionCount
}
}
`,
Variables: map[string]any{
"user": model.NewId(),
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 1)
})
t.Run("pagination", func(t *testing.T) {
query := `query channelMembers($first: Int, $after: String = "") {
channelMembers(userId: "me", first: $first, after: $after) {
channel {
id
createAt
updateAt
type
displayName
name
header
}
cursor
}
}
`
input := graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"first": 4,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelMembers, 4)
input = graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"first": 4,
"after": q.ChannelMembers[3].Cursor,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelMembers, 4)
input = graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"first": 4,
"after": q.ChannelMembers[3].Cursor,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelMembers, 1)
})
t.Run("channel_filter", func(t *testing.T) {
query := `query channelMembers($channelId: String, $first: Int, $after: String = "") {
channelMembers(userId: "me", channelId: $channelId, first: $first, after: $after) {
channel {
id
}
}
}
`
input := graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"channelId": ch1.Id,
"first": 4,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelMembers, 1)
assert.Equal(t, q.ChannelMembers[0].Channel.ID, ch1.Id)
input = graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"channelId": model.NewId(),
"first": 3,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 1)
})
t.Run("team_filter", func(t *testing.T) {
query := `query channelMembers($teamId: String, $excludeTeam: Boolean = false) {
channelMembers(userId: "me", teamId: $teamId, excludeTeam: $excludeTeam) {
channel {
id
}
}
}
`
input := graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"teamId": th.BasicTeam.Id,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelMembers, 5)
input = graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"teamId": th.BasicTeam.Id,
"excludeTeam": true,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelMembers, 4)
})
t.Run("UpdateAt", func(t *testing.T) {
query := `query channelMembers($first: Int, $after: String = "", $lastUpdateAt: Float) {
channelMembers(userId: "me", first: $first, after: $after, lastUpdateAt: $lastUpdateAt) {
channel {
id
}
lastUpdateAt
cursor
}
}
`
now := model.GetMillis()
input := graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"first": 4,
"lastUpdateAt": float64(now),
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
require.Len(t, q.ChannelMembers, 0)
// Create post to update the lastUpdateAt for the channel member.
th.CreateMessagePostWithClient(th.Client, th.BasicChannel, "another post")
input = graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"first": 4,
"lastUpdateAt": float64(now),
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
require.Len(t, q.ChannelMembers, 1)
assert.Equal(t, th.BasicChannel.Id, q.ChannelMembers[0].Channel.ID)
assert.GreaterOrEqual(t, q.ChannelMembers[0].LastUpdateAt, float64(now))
})
}
func TestChannelMemberCursor(t *testing.T) {
ch := channelMember{
ChannelMember: model.ChannelMember{ChannelId: "testid", UserId: "userid"},
}
cur := ch.Cursor()
chId, userId, ok := parseChannelMemberCursor(*cur)
require.True(t, ok)
assert.Equal(t, ch.ChannelId, chId)
assert.Equal(t, ch.UserId, userId)
}

514
server/channels/api4/resolver_channel_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,514 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGraphQLChannels(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t).InitBasic()
defer th.TearDown()
// Adding another team with more channels (public and private)
myTeam := th.CreateTeam()
ch1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, myTeam.Id)
ch2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypePrivate, myTeam.Id)
th.LinkUserToTeam(th.BasicUser, myTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch2, false)
th.CreateDmChannel(th.BasicUser2)
var q struct {
Channels []struct {
ID string `json:"id"`
CreateAt float64 `json:"createAt"`
UpdateAt float64 `json:"updateAt"`
Type model.ChannelType `json:"type"`
DisplayName string `json:"displayName"`
PrettyDisplayName string `json:"prettyDisplayName"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
SchemeId string `json:"schemeId"`
TotalMsgCountRoot float64 `json:"totalMsgCountRoot"`
LastRootPostAt float64 `json:"lastRootPostAt"`
Cursor string `json:"cursor"`
Props map[string]any `json:"props"`
Team struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
} `json:"team"`
} `json:"channels"`
}
t.Run("all", func(t *testing.T) {
input := graphQLInput{
OperationName: "channels",
Query: `
query channels {
channels(userId: "me") {
id
createAt
updateAt
type
displayName
prettyDisplayName
name
header
purpose
schemeId
totalMsgCountRoot
lastRootPostAt
cursor
props
}
}
`,
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 10)
numPrivate := 0
numPublic := 0
numOffTopic := 0
numTownSquare := 0
for _, ch := range q.Channels {
assert.NotEmpty(t, ch.ID)
assert.NotEmpty(t, ch.Name)
assert.NotEmpty(t, ch.Cursor)
assert.NotEmpty(t, ch.PrettyDisplayName)
assert.NotEmpty(t, ch.CreateAt)
assert.NotEmpty(t, ch.UpdateAt)
assert.NotNil(t, ch.Props)
if ch.Type == model.ChannelTypeOpen {
numPublic++
} else if ch.Type == model.ChannelTypePrivate {
numPrivate++
}
if ch.DisplayName == "Off-Topic" {
numOffTopic++
} else if ch.DisplayName == "Town Square" {
numTownSquare++
}
}
assert.Equal(t, 2, numPrivate)
assert.Equal(t, 7, numPublic)
assert.Equal(t, 2, numOffTopic)
assert.Equal(t, 2, numTownSquare)
})
t.Run("user_perms", func(t *testing.T) {
query := `query channels($userId: String = "") {
channels(userId: $userId) {
id
createAt
updateAt
type
cursor
}
}
`
u1 := th.CreateUser()
input := graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"userId": u1.Id,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 1)
})
t.Run("pagination", func(t *testing.T) {
query := `query channels($first: Int, $after: String = "") {
channels(userId: "me", first: $first, after: $after) {
id
createAt
updateAt
type
displayName
name
header
purpose
schemeId
cursor
}
}
`
input := graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"first": 4,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 4)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"first": 4,
"after": q.Channels[3].Cursor,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 4)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"first": 4,
"after": q.Channels[3].Cursor,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 2)
})
t.Run("team_filter", func(t *testing.T) {
query := `query channels($teamId: String, $first: Int) {
channels(userId: "me", teamId: $teamId, first: $first) {
id
}
}
`
input := graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"first": 10,
"teamId": myTeam.Id,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 5)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"first": 2,
"teamId": myTeam.Id,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 2)
})
t.Run("team_data", func(t *testing.T) {
query := `query channels($teamId: String, $first: Int) {
channels(userId: "me", teamId: $teamId, first: $first) {
id
team {
id
displayName
}
}
}
`
input := graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"first": 2,
"teamId": myTeam.Id,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 2)
// Iterating because one of them can be a DM channel.
for _, ch := range q.Channels {
if ch.Team.ID != "" {
assert.Equal(t, myTeam.Id, ch.Team.ID)
assert.Equal(t, myTeam.DisplayName, ch.Team.DisplayName)
}
}
})
t.Run("Delete+Update", func(t *testing.T) {
query := `query channels($lastDeleteAt: Float = 0,
$lastUpdateAt: Float = 0,
$first: Int = 60,
$includeDeleted: Boolean) {
channels(userId: "me", lastDeleteAt: $lastDeleteAt, lastUpdateAt: $lastUpdateAt, first: $first, includeDeleted: $includeDeleted) {
id
}
}
`
input := graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"includeDeleted": false,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 10)
now := model.GetMillis()
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"includeDeleted": true,
"lastUpdateAt": float64(now),
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0) // no errors for no channels found
th.BasicChannel.Purpose = "newpurpose"
_, _, err = th.Client.UpdateChannel(th.BasicChannel)
require.NoError(t, err)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"includeDeleted": true,
"lastUpdateAt": float64(now),
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 1)
_, err = th.Client.DeleteChannel(ch1.Id)
require.NoError(t, err)
_, err = th.Client.DeleteChannel(ch2.Id)
require.NoError(t, err)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"includeDeleted": false,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 8)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"includeDeleted": true,
"lastDeleteAt": float64(model.GetMillis()),
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 8)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"includeDeleted": true,
"lastDeleteAt": float64(model.GetMillis()),
"first": 5,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.Channels, 5)
})
}
func TestGetPrettyDNForUsers(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
t.Run("nickname_full_name", func(t *testing.T) {
users := []*model.User{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "nick2", getPrettyDNForUsers("nickname_full_name", users, "user1", map[string]string{}))
users = []*model.User{
{
Id: "user1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "first2 last2", getPrettyDNForUsers("nickname_full_name", users, "user1", map[string]string{}))
})
t.Run("full_name", func(t *testing.T) {
users := []*model.User{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "first2 last2", getPrettyDNForUsers("full_name", users, "user1", map[string]string{}))
users = []*model.User{
{
Id: "user1",
Username: "user1",
},
{
Id: "user2",
Username: "user2",
},
}
assert.Equal(t, "user2", getPrettyDNForUsers("full_name", users, "user1", map[string]string{}))
})
t.Run("username", func(t *testing.T) {
users := []*model.User{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "user2", getPrettyDNForUsers("username", users, "user1", map[string]string{}))
})
t.Run("cache", func(t *testing.T) {
users := []*model.User{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
cache := map[string]string{}
assert.Equal(t, "first2 last2", getPrettyDNForUsers("full_name", users, "user1", cache))
cache["user2"] = "teststring!!"
assert.Equal(t, "teststring!!", getPrettyDNForUsers("full_name", users, "user1", cache))
})
}
func TestChannelCursor(t *testing.T) {
ch := channel{
Channel: model.Channel{Id: "testid"},
}
cur := ch.Cursor()
id, ok := parseChannelCursor(*cur)
require.True(t, ok)
assert.Equal(t, ch.Id, id)
}

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

@@ -0,0 +1,141 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"os"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGraphQLSidebarCategories(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t).InitBasic()
defer th.TearDown()
var q struct {
SidebarCategories []struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Sorting model.SidebarCategorySorting `json:"sorting"`
ChannelIDs []string `json:"channelIds"`
TeamID string `json:"teamId"`
SortOrder int64 `json:"sortOrder"`
} `json:"sidebarCategories"`
}
input := graphQLInput{
OperationName: "sidebarCategories",
Query: `
query sidebarCategories($userId: String = "", $teamId: String = "", $excludeTeam: Boolean = false) {
sidebarCategories(userId: $userId, teamId: $teamId, excludeTeam: $excludeTeam) {
id
displayName
sorting
channelIds
sortOrder
}
}
`,
Variables: map[string]any{
"userId": "me",
"teamId": th.BasicTeam.Id,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.SidebarCategories, 3)
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
sort.Slice(q.SidebarCategories, func(i, j int) bool {
return q.SidebarCategories[i].ID < q.SidebarCategories[j].ID
})
sort.Slice(categories.Categories, func(i, j int) bool {
return categories.Categories[i].Id < categories.Categories[j].Id
})
for i := range categories.Categories {
assert.Equal(t, categories.Categories[i].Id, q.SidebarCategories[i].ID)
assert.Equal(t, categories.Categories[i].DisplayName, q.SidebarCategories[i].DisplayName)
assert.Equal(t, categories.Categories[i].Sorting, q.SidebarCategories[i].Sorting)
assert.Equal(t, categories.Categories[i].ChannelIds(), q.SidebarCategories[i].ChannelIDs)
assert.Equal(t, categories.Categories[i].SortOrder, q.SidebarCategories[i].SortOrder)
}
input = graphQLInput{
OperationName: "sidebarCategories",
Query: `
query sidebarCategories($userId: String = "", $teamId: String = "", $excludeTeam: Boolean = false) {
sidebarCategories(userId: $userId, teamId: $teamId, excludeTeam: $excludeTeam) {
id
displayName
sorting
channelIds
sortOrder
}
}
`,
Variables: map[string]any{
"userId": "me",
"teamId": th.BasicTeam.Id,
"excludeTeam": true,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.SidebarCategories, 0)
// Adding a new team
myTeam := th.CreateTeam()
ch1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, myTeam.Id)
ch2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypePrivate, myTeam.Id)
th.LinkUserToTeam(th.BasicUser, myTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch2, false)
input = graphQLInput{
OperationName: "sidebarCategories",
Query: `
query sidebarCategories($userId: String = "", $teamId: String = "", $excludeTeam: Boolean = false) {
sidebarCategories(userId: $userId, teamId: $teamId, excludeTeam: $excludeTeam) {
id
displayName
sorting
channelIds
teamId
sortOrder
}
}
`,
Variables: map[string]any{
"userId": "me",
"teamId": th.BasicTeam.Id,
"excludeTeam": true,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.SidebarCategories, 3)
for _, cat := range q.SidebarCategories {
assert.Equal(t, myTeam.Id, cat.TeamID)
}
}

95
server/channels/api4/resolver_team.go Обычный файл
Просмотреть файл

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
func getGraphQLTeam(ctx context.Context, id string) (*model.Team, error) {
loader, err := getTeamsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(id))
result, err := thunk()
if err != nil {
return nil, err
}
team := result.(*model.Team)
return team, nil
}
func graphQLTeamsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
teams, err := getGraphQLTeams(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range teams {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLTeams(c *web.Context, teamIDs []string) ([]*model.Team, error) {
teams, appErr := c.App.GetTeams(teamIDs)
if appErr != nil {
return nil, appErr
}
if len(teams) != len(teamIDs) {
return nil, fmt.Errorf("all teams were not found. Requested %d; Found %d", len(teamIDs), len(teams))
}
var teamsToCheck []string
for _, team := range teams {
if !team.AllowOpenInvite || team.Type != model.TeamOpen {
teamsToCheck = append(teamsToCheck, team.Id)
}
}
if !c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsToCheck, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
for i, team := range teams {
teams[i] = c.App.SanitizeTeam(*c.AppContext.Session(), team)
}
// The teams need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Team, len(teams))
for _, ch := range teams {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the teams.
for i, id := range teamIDs {
teams[i] = tmp[id]
}
return teams, nil
}

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

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"strings"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
)
// teamMember is an internal graphQL wrapper struct to add resolver methods.
type teamMember struct {
model.TeamMember
}
// match with api4.getTeam
func (tm *teamMember) Team(ctx context.Context) (*model.Team, error) {
return getGraphQLTeam(ctx, tm.TeamId)
}
// match with api4.getUser
func (tm *teamMember) User(ctx context.Context) (*user, error) {
return getGraphQLUser(ctx, tm.UserId)
}
// match with api4.getRolesByNames
func (tm *teamMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(tm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}

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

@@ -0,0 +1,412 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"os"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGraphQLTeamMembers(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t).InitBasic()
defer th.TearDown()
var q struct {
TeamMembers []struct {
User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
NickName string `json:"nickname"`
} `json:"user"`
Team struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Name string `json:"name"`
CreateAt float64 `json:"createAt"`
DeleteAt float64 `json:"deleteAt"`
SchemeId *string `json:"schemeId"`
PolicyId *string `json:"policyId"`
CloudLimitsArchived bool `json:"cloudLimitsArchived"`
} `json:"team"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
Permissions []string `json:"permissions"`
SchemeManaged bool `json:"schemeManaged"`
BuiltIn bool `json:"builtIn"`
} `json:"roles"`
DeleteAt float64 `json:"deleteAt"`
SchemeGuest bool `json:"schemeGuest"`
SchemeUser bool `json:"schemeUser"`
SchemeAdmin bool `json:"schemeAdmin"`
} `json:"teamMembers"`
}
t.Run("User", func(t *testing.T) {
input := graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId) {
team {
id
displayName
}
user {
id
username
email
firstName
lastName
}
roles {
id
name
}
schemeGuest
schemeUser
schemeAdmin
}
}
`,
Variables: map[string]any{
"userId": "me",
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.TeamMembers, 1)
tm := q.TeamMembers[0]
assert.Equal(t, th.BasicTeam.Id, tm.Team.ID)
assert.Equal(t, th.BasicTeam.DisplayName, tm.Team.DisplayName)
assert.Equal(t, th.BasicUser.Id, tm.User.ID)
assert.Equal(t, th.BasicUser.Username, tm.User.Username)
assert.Equal(t, th.BasicUser.Email, tm.User.Email)
assert.Equal(t, th.BasicUser.FirstName, tm.User.FirstName)
assert.Equal(t, th.BasicUser.LastName, tm.User.LastName)
require.Len(t, tm.Roles, 1)
assert.NotEmpty(t, tm.Roles[0].ID)
assert.Equal(t, "team_user", tm.Roles[0].Name)
assert.False(t, tm.SchemeGuest)
assert.True(t, tm.SchemeUser)
assert.False(t, tm.SchemeAdmin)
})
t.Run("User+Team", func(t *testing.T) {
input := graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId) {
team {
id
displayName
name
createAt
deleteAt
schemeId
policyId
cloudLimitsArchived
}
user {
id
username
email
firstName
lastName
}
roles {
id
name
}
}
}
`,
Variables: map[string]any{
"userId": "me",
"teamId": th.BasicTeam.Id,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.TeamMembers, 1)
tm := q.TeamMembers[0]
assert.Equal(t, th.BasicTeam.Id, tm.Team.ID)
assert.Equal(t, th.BasicTeam.DisplayName, tm.Team.DisplayName)
assert.Equal(t, th.BasicTeam.Name, tm.Team.Name)
assert.Equal(t, th.BasicTeam.CreateAt_(), tm.Team.CreateAt)
assert.Equal(t, th.BasicTeam.DeleteAt_(), tm.Team.DeleteAt)
assert.Equal(t, th.BasicTeam.SchemeId, tm.Team.SchemeId)
assert.Equal(t, th.BasicTeam.PolicyID, tm.Team.PolicyId)
assert.Equal(t, th.BasicTeam.CloudLimitsArchived, tm.Team.CloudLimitsArchived)
assert.Equal(t, th.BasicUser.Id, tm.User.ID)
assert.Equal(t, th.BasicUser.Username, tm.User.Username)
assert.Equal(t, th.BasicUser.Email, tm.User.Email)
assert.Equal(t, th.BasicUser.FirstName, tm.User.FirstName)
assert.Equal(t, th.BasicUser.LastName, tm.User.LastName)
require.Len(t, tm.Roles, 1)
assert.NotEmpty(t, tm.Roles[0].ID)
assert.Equal(t, "team_user", tm.Roles[0].Name)
})
t.Run("NewTeam", func(t *testing.T) {
// Adding another team with more channels (public and private)
myTeam := th.CreateTeam()
ch1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, myTeam.Id)
ch2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypePrivate, myTeam.Id)
th.LinkUserToTeam(th.BasicUser, myTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch2, false)
input := graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId) {
team {
id
displayName
}
roles {
id
name
}
}
}
`,
Variables: map[string]any{
"userId": "me",
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.TeamMembers, 2)
sort.Slice(q.TeamMembers, func(i, j int) bool {
return q.TeamMembers[i].Team.ID < q.TeamMembers[j].Team.ID
})
expectedTeams := []*model.Team{th.BasicTeam, myTeam}
sort.Slice(expectedTeams, func(i, j int) bool {
return expectedTeams[i].Id < expectedTeams[j].Id
})
for i := range q.TeamMembers {
tm := q.TeamMembers[i]
if tm.Team.ID == myTeam.Id {
require.Len(t, tm.Roles, 2)
sort.Slice(tm.Roles, func(i, j int) bool {
return tm.Roles[i].Name < tm.Roles[j].Name
})
assert.Equal(t, "team_admin", tm.Roles[0].Name)
assert.Equal(t, "team_user", tm.Roles[1].Name)
} else {
require.Len(t, tm.Roles, 1)
assert.NotEmpty(t, tm.Roles[0].ID)
assert.Equal(t, "team_user", tm.Roles[0].Name)
}
expectedTeams[i].Id = tm.Team.ID
expectedTeams[i].DisplayName = tm.Team.DisplayName
}
// Negate team
input = graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId, excludeTeam: true) {
team {
id
displayName
}
}
}
`,
Variables: map[string]any{
"userId": "me",
"teamId": th.BasicTeam.Id,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.TeamMembers, 1)
input = graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId) {
team {
id
displayName
}
}
}
`,
Variables: map[string]any{
"userId": "me",
},
}
// Removing from a team and ensuring we get the right response.
th.UnlinkUserFromTeam(th.BasicUser, myTeam)
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.TeamMembers, 1)
})
}
func TestGraphQLTeamMembersAsGuest(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t)
id := model.NewId()
team := &model.Team{
DisplayName: "dn_" + id,
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
AllowOpenInvite: true,
}
var err error
team, _, err = th.Client.CreateTeam(team)
require.NoError(t, err)
th.BasicTeam = team
th.BasicChannel = th.CreatePublicChannel()
th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel, false)
th.LoginBasic()
defer th.TearDown()
require.Nil(t, th.App.DemoteUserToGuest(th.Context, th.BasicUser))
var q struct {
TeamMembers []struct {
User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
NickName string `json:"nickname"`
} `json:"user"`
Team struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Name string `json:"name"`
CreateAt float64 `json:"createAt"`
DeleteAt float64 `json:"deleteAt"`
SchemeId *string `json:"schemeId"`
PolicyId *string `json:"policyId"`
CloudLimitsArchived bool `json:"cloudLimitsArchived"`
} `json:"team"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
Permissions []string `json:"permissions"`
SchemeManaged bool `json:"schemeManaged"`
BuiltIn bool `json:"builtIn"`
} `json:"roles"`
DeleteAt float64 `json:"deleteAt"`
SchemeGuest bool `json:"schemeGuest"`
SchemeUser bool `json:"schemeUser"`
SchemeAdmin bool `json:"schemeAdmin"`
} `json:"teamMembers"`
}
t.Run("User", func(t *testing.T) {
input := graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId) {
team {
id
displayName
}
user {
id
username
email
firstName
lastName
}
roles {
id
name
}
schemeGuest
schemeUser
schemeAdmin
}
}
`,
Variables: map[string]any{
"userId": "me",
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.TeamMembers, 1)
tm := q.TeamMembers[0]
assert.Equal(t, th.BasicTeam.Id, tm.Team.ID)
assert.Equal(t, th.BasicTeam.DisplayName, tm.Team.DisplayName)
assert.Equal(t, th.BasicUser.Id, tm.User.ID)
assert.Equal(t, th.BasicUser.Username, tm.User.Username)
assert.Equal(t, th.BasicUser.Email, tm.User.Email)
assert.Equal(t, th.BasicUser.FirstName, tm.User.FirstName)
assert.Equal(t, th.BasicUser.LastName, tm.User.LastName)
require.Len(t, tm.Roles, 1)
assert.NotEmpty(t, tm.Roles[0].ID)
assert.Equal(t, "team_guest", tm.Roles[0].Name)
assert.True(t, tm.SchemeGuest)
assert.False(t, tm.SchemeUser)
assert.False(t, tm.SchemeAdmin)
})
}

228
server/channels/api4/resolver_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,228 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGraphQLConfig(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t)
th.LoginBasicWithGraphQL()
defer th.TearDown()
var q struct {
Config map[string]string `json:"config"`
}
input := graphQLInput{
OperationName: "config",
Query: `
query config {
config
}
`,
}
cfg, _, err := th.Client.GetOldClientConfig("")
require.NoError(t, err)
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Equal(t, cfg, q.Config)
}
func TestGraphQLLicense(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t)
th.LoginBasicWithGraphQL()
defer th.TearDown()
var q struct {
License map[string]string `json:"license"`
}
input := graphQLInput{
OperationName: "license",
Query: `
query license {
license
}
`,
}
cfg, _, err := th.Client.GetOldClientLicense("")
require.NoError(t, err)
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Equal(t, cfg, q.License)
}
func TestGraphQLChannelsLeft(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t).InitBasic()
defer th.TearDown()
var q struct {
ChannelsLeft []string `json:"channelsLeft"`
}
t.Run("NotLeft", func(t *testing.T) {
input := graphQLInput{
OperationName: "channelsLeft",
Query: `
query channelsLeft($userId: String = "me", $since: Float = 0.0) {
channelsLeft(userId: $userId, since: $since)
}
`,
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelsLeft, 0)
})
t.Run("Left", func(t *testing.T) {
_, err := th.Client.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
input := graphQLInput{
OperationName: "channelsLeft",
Query: `
query channelsLeft($userId: String = "me", $since: Float = 0.0) {
channelsLeft(userId: $userId, since: $since)
}
`,
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelsLeft, 1)
})
t.Run("LeftAfterTime", func(t *testing.T) {
input := graphQLInput{
OperationName: "channelsLeft",
Query: `
query channelsLeft($userId: String = "me", $since: Float = 0.0) {
channelsLeft(userId: $userId, since: $since)
}
`,
Variables: map[string]any{
"since": model.GetMillis(),
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
assert.Len(t, q.ChannelsLeft, 0)
})
}
func TestGraphQLRolesLoader(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t).InitBasic()
defer th.TearDown()
var q struct {
User struct {
ID string `json:"id"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
} `json:"roles"`
} `json:"user"`
ChannelMembers []struct {
MsgCount float64 `json:"msgCount"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
} `json:"roles"`
} `json:"channelMembers"`
TeamMembers []struct {
SchemeUser bool `json:"schemeUser"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
} `json:"roles"`
}
}
input := graphQLInput{
OperationName: "channelMembers",
Query: `
query channelMembers {
user(id: "me") {
id
username
roles {
id
name
}
}
channelMembers(userId: "me") {
msgCount
roles {
id
name
}
}
teamMembers(userId: "me") {
schemeUser
roles {
id
name
}
}
}
`,
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0)
require.NoError(t, json.Unmarshal(resp.Data, &q))
require.Len(t, q.User.Roles, 1)
assert.Equal(t, "system_user", q.User.Roles[0].Name)
require.Len(t, q.ChannelMembers, 5)
for _, cm := range q.ChannelMembers {
require.Len(t, cm.Roles, 1)
assert.Equal(t, "channel_user", cm.Roles[0].Name)
}
require.Len(t, q.TeamMembers, 1)
for _, tm := range q.TeamMembers {
require.Len(t, tm.Roles, 1)
assert.Equal(t, "team_user", tm.Roles[0].Name)
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше