Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
435
server/channels/api4/api.go
Обычный файл
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
Обычный файл
1341
server/channels/api4/apitestlib.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
39
server/channels/api4/bleve.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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
1359
server/channels/api4/bot_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
97
server/channels/api4/brand.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
Обычный файл
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
Обычный файл
2137
server/channels/api4/channel.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
333
server/channels/api4/channel_category.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
Обычный файл
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
Обычный файл
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
Обычный файл
4624
server/channels/api4/channel_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
804
server/channels/api4/cloud.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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
1067
server/channels/api4/command_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
487
server/channels/api4/commands_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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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(¶ms)
|
||||
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
Обычный файл
1267
server/channels/api4/file_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
193
server/channels/api4/graphql.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(¶ms); 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
Обычный файл
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
Обычный файл
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
Обычный файл
1351
server/channels/api4/group.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
41
server/channels/api4/group_local.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
Обычный файл
1688
server/channels/api4/group_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
226
server/channels/api4/handlers.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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
1243
server/channels/api4/insights_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
130
server/channels/api4/integration_action.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)
|
||||
}
|
||||
275
server/channels/api4/integration_action_test.go
Обычный файл
275
server/channels/api4/integration_action_test.go
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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(¬ifyAdminRequest)
|
||||
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(¬ifyAdminRequest)
|
||||
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
1886
server/channels/api4/plugin_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1124
server/channels/api4/post.go
Обычный файл
1124
server/channels/api4/post.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
10
server/channels/api4/post_local.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
Обычный файл
3799
server/channels/api4/post_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
164
server/channels/api4/preference.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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
|
||||
}
|
||||
226
server/channels/api4/resolver_channel_member.go
Обычный файл
226
server/channels/api4/resolver_channel_member.go
Обычный файл
@@ -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
|
||||
}
|
||||
400
server/channels/api4/resolver_channel_member_test.go
Обычный файл
400
server/channels/api4/resolver_channel_member_test.go
Обычный файл
@@ -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
Обычный файл
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)
|
||||
}
|
||||
141
server/channels/api4/resolver_sidebar_categories_test.go
Обычный файл
141
server/channels/api4/resolver_sidebar_categories_test.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"
|
||||
"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
Обычный файл
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
|
||||
}
|
||||
50
server/channels/api4/resolver_team_member.go
Обычный файл
50
server/channels/api4/resolver_team_member.go
Обычный файл
@@ -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
|
||||
}
|
||||
412
server/channels/api4/resolver_team_member_test.go
Обычный файл
412
server/channels/api4/resolver_team_member_test.go
Обычный файл
@@ -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
Обычный файл
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)
|
||||
}
|
||||
}
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user