From 11cbb597471127c1b29e78e6cad0a1a4d93ea24c Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Thu, 17 May 2018 12:40:40 -0700 Subject: Renaming platform binary to mattermost. (#8801) --- cmd/cmd.go | 26 - cmd/cmdtestlib.go | 45 -- cmd/commands/channel.go | 496 ----------------- cmd/commands/channel_test.go | 116 ---- cmd/commands/channelargs.go | 60 --- cmd/commands/command.go | 70 --- cmd/commands/commandargs.go | 64 --- cmd/commands/config.go | 68 --- cmd/commands/config_flag_test.go | 49 -- cmd/commands/config_test.go | 31 -- cmd/commands/exec_command_test.go | 21 - cmd/commands/import.go | 145 ----- cmd/commands/jobserver.go | 63 --- cmd/commands/ldap.go | 78 --- cmd/commands/license.go | 55 -- cmd/commands/message_export.go | 82 --- cmd/commands/message_export_test.go | 67 --- cmd/commands/permissions.go | 66 --- cmd/commands/reset.go | 54 -- cmd/commands/roles.go | 90 ---- cmd/commands/roles_test.go | 28 - cmd/commands/sampledata.go | 637 ---------------------- cmd/commands/sampledata_test.go | 26 - cmd/commands/server.go | 296 ---------- cmd/commands/server_test.go | 136 ----- cmd/commands/team.go | 250 --------- cmd/commands/team_test.go | 98 ---- cmd/commands/teamargs.go | 33 -- cmd/commands/test.go | 148 ----- cmd/commands/user.go | 716 ------------------------- cmd/commands/user_test.go | 122 ----- cmd/commands/userargs.go | 39 -- cmd/commands/version.go | 45 -- cmd/commands/version_test.go | 14 - cmd/init.go | 46 -- cmd/mattermost/commands/channel.go | 495 +++++++++++++++++ cmd/mattermost/commands/channel_test.go | 115 ++++ cmd/mattermost/commands/channelargs.go | 60 +++ cmd/mattermost/commands/cmdtestlib.go | 45 ++ cmd/mattermost/commands/command.go | 69 +++ cmd/mattermost/commands/commandargs.go | 64 +++ cmd/mattermost/commands/config.go | 67 +++ cmd/mattermost/commands/config_flag_test.go | 48 ++ cmd/mattermost/commands/config_test.go | 30 ++ cmd/mattermost/commands/exec_command_test.go | 19 + cmd/mattermost/commands/import.go | 144 +++++ cmd/mattermost/commands/init.go | 46 ++ cmd/mattermost/commands/jobserver.go | 62 +++ cmd/mattermost/commands/ldap.go | 77 +++ cmd/mattermost/commands/license.go | 54 ++ cmd/mattermost/commands/message_export.go | 81 +++ cmd/mattermost/commands/message_export_test.go | 66 +++ cmd/mattermost/commands/output.go | 21 + cmd/mattermost/commands/permissions.go | 64 +++ cmd/mattermost/commands/reset.go | 53 ++ cmd/mattermost/commands/roles.go | 89 +++ cmd/mattermost/commands/roles_test.go | 27 + cmd/mattermost/commands/root.go | 28 + cmd/mattermost/commands/sampledata.go | 636 ++++++++++++++++++++++ cmd/mattermost/commands/sampledata_test.go | 25 + cmd/mattermost/commands/server.go | 299 +++++++++++ cmd/mattermost/commands/server_test.go | 136 +++++ cmd/mattermost/commands/team.go | 249 +++++++++ cmd/mattermost/commands/team_test.go | 97 ++++ cmd/mattermost/commands/teamargs.go | 33 ++ cmd/mattermost/commands/test.go | 147 +++++ cmd/mattermost/commands/user.go | 715 ++++++++++++++++++++++++ cmd/mattermost/commands/user_test.go | 121 +++++ cmd/mattermost/commands/userargs.go | 39 ++ cmd/mattermost/commands/version.go | 44 ++ cmd/mattermost/commands/version_test.go | 12 + cmd/mattermost/main.go | 33 ++ cmd/output.go | 21 - cmd/platform/main.go | 39 ++ 74 files changed, 4449 insertions(+), 4401 deletions(-) delete mode 100644 cmd/cmd.go delete mode 100644 cmd/cmdtestlib.go delete mode 100644 cmd/commands/channel.go delete mode 100644 cmd/commands/channel_test.go delete mode 100644 cmd/commands/channelargs.go delete mode 100644 cmd/commands/command.go delete mode 100644 cmd/commands/commandargs.go delete mode 100644 cmd/commands/config.go delete mode 100644 cmd/commands/config_flag_test.go delete mode 100644 cmd/commands/config_test.go delete mode 100644 cmd/commands/exec_command_test.go delete mode 100644 cmd/commands/import.go delete mode 100644 cmd/commands/jobserver.go delete mode 100644 cmd/commands/ldap.go delete mode 100644 cmd/commands/license.go delete mode 100644 cmd/commands/message_export.go delete mode 100644 cmd/commands/message_export_test.go delete mode 100644 cmd/commands/permissions.go delete mode 100644 cmd/commands/reset.go delete mode 100644 cmd/commands/roles.go delete mode 100644 cmd/commands/roles_test.go delete mode 100644 cmd/commands/sampledata.go delete mode 100644 cmd/commands/sampledata_test.go delete mode 100644 cmd/commands/server.go delete mode 100644 cmd/commands/server_test.go delete mode 100644 cmd/commands/team.go delete mode 100644 cmd/commands/team_test.go delete mode 100644 cmd/commands/teamargs.go delete mode 100644 cmd/commands/test.go delete mode 100644 cmd/commands/user.go delete mode 100644 cmd/commands/user_test.go delete mode 100644 cmd/commands/userargs.go delete mode 100644 cmd/commands/version.go delete mode 100644 cmd/commands/version_test.go delete mode 100644 cmd/init.go create mode 100644 cmd/mattermost/commands/channel.go create mode 100644 cmd/mattermost/commands/channel_test.go create mode 100644 cmd/mattermost/commands/channelargs.go create mode 100644 cmd/mattermost/commands/cmdtestlib.go create mode 100644 cmd/mattermost/commands/command.go create mode 100644 cmd/mattermost/commands/commandargs.go create mode 100644 cmd/mattermost/commands/config.go create mode 100644 cmd/mattermost/commands/config_flag_test.go create mode 100644 cmd/mattermost/commands/config_test.go create mode 100644 cmd/mattermost/commands/exec_command_test.go create mode 100644 cmd/mattermost/commands/import.go create mode 100644 cmd/mattermost/commands/init.go create mode 100644 cmd/mattermost/commands/jobserver.go create mode 100644 cmd/mattermost/commands/ldap.go create mode 100644 cmd/mattermost/commands/license.go create mode 100644 cmd/mattermost/commands/message_export.go create mode 100644 cmd/mattermost/commands/message_export_test.go create mode 100644 cmd/mattermost/commands/output.go create mode 100644 cmd/mattermost/commands/permissions.go create mode 100644 cmd/mattermost/commands/reset.go create mode 100644 cmd/mattermost/commands/roles.go create mode 100644 cmd/mattermost/commands/roles_test.go create mode 100644 cmd/mattermost/commands/root.go create mode 100644 cmd/mattermost/commands/sampledata.go create mode 100644 cmd/mattermost/commands/sampledata_test.go create mode 100644 cmd/mattermost/commands/server.go create mode 100644 cmd/mattermost/commands/server_test.go create mode 100644 cmd/mattermost/commands/team.go create mode 100644 cmd/mattermost/commands/team_test.go create mode 100644 cmd/mattermost/commands/teamargs.go create mode 100644 cmd/mattermost/commands/test.go create mode 100644 cmd/mattermost/commands/user.go create mode 100644 cmd/mattermost/commands/user_test.go create mode 100644 cmd/mattermost/commands/userargs.go create mode 100644 cmd/mattermost/commands/version.go create mode 100644 cmd/mattermost/commands/version_test.go create mode 100644 cmd/mattermost/main.go delete mode 100644 cmd/output.go create mode 100644 cmd/platform/main.go (limited to 'cmd') diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index 5a1a25bd9..000000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package cmd - -import ( - "github.com/spf13/cobra" -) - -type Command = cobra.Command - -func Run(args []string) error { - RootCmd.SetArgs(args) - return RootCmd.Execute() -} - -var RootCmd = &cobra.Command{ - Use: "platform", - Short: "Open source, self-hosted Slack-alternative", - Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`, -} - -func init() { - RootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.") - RootCmd.PersistentFlags().Bool("disableconfigwatch", false, "When set config.json will not be loaded from disk when the file is changed.") -} diff --git a/cmd/cmdtestlib.go b/cmd/cmdtestlib.go deleted file mode 100644 index db97b1a41..000000000 --- a/cmd/cmdtestlib.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package cmd - -import ( - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -var coverprofileCounters map[string]int = make(map[string]int) - -func execArgs(t *testing.T, args []string) []string { - ret := []string{"-test.run", "ExecCommand"} - if coverprofile := flag.Lookup("test.coverprofile").Value.String(); coverprofile != "" { - dir := filepath.Dir(coverprofile) - base := filepath.Base(coverprofile) - baseParts := strings.SplitN(base, ".", 2) - coverprofileCounters[t.Name()] = coverprofileCounters[t.Name()] + 1 - baseParts[0] = fmt.Sprintf("%v-%v-%v", baseParts[0], t.Name(), coverprofileCounters[t.Name()]) - ret = append(ret, "-test.coverprofile", filepath.Join(dir, strings.Join(baseParts, "."))) - } - return append(append(ret, "--", "--disableconfigwatch"), args...) -} - -func CheckCommand(t *testing.T, args ...string) string { - path, err := os.Executable() - require.NoError(t, err) - output, err := exec.Command(path, execArgs(t, args)...).CombinedOutput() - require.NoError(t, err, string(output)) - return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(string(output)), "PASS")) -} - -func RunCommand(t *testing.T, args ...string) error { - path, err := os.Executable() - require.NoError(t, err) - return exec.Command(path, execArgs(t, args)...).Run() -} diff --git a/cmd/commands/channel.go b/cmd/commands/channel.go deleted file mode 100644 index 30d4e53ec..000000000 --- a/cmd/commands/channel.go +++ /dev/null @@ -1,496 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/spf13/cobra" -) - -var ChannelCmd = &cobra.Command{ - Use: "channel", - Short: "Management of channels", -} - -var ChannelCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a channel", - Long: `Create a channel.`, - Example: ` channel create --team myteam --name mynewchannel --display_name "My New Channel" - channel create --team myteam --name mynewprivatechannel --display_name "My New Private Channel" --private`, - RunE: createChannelCmdF, -} - -var RemoveChannelUsersCmd = &cobra.Command{ - Use: "remove [channel] [users]", - Short: "Remove users from channel", - Long: "Remove some users from channel", - Example: " channel remove myteam:mychannel user@example.com username", - RunE: removeChannelUsersCmdF, -} - -var AddChannelUsersCmd = &cobra.Command{ - Use: "add [channel] [users]", - Short: "Add users to channel", - Long: "Add some users to channel", - Example: " channel add myteam:mychannel user@example.com username", - RunE: addChannelUsersCmdF, -} - -var ArchiveChannelsCmd = &cobra.Command{ - Use: "archive [channels]", - Short: "Archive channels", - Long: `Archive some channels. -Archive a channel along with all related information including posts from the database. -Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, - Example: " channel archive myteam:mychannel", - RunE: archiveChannelsCmdF, -} - -var DeleteChannelsCmd = &cobra.Command{ - Use: "delete [channels]", - Short: "Delete channels", - Long: `Permanently delete some channels. -Permanently deletes a channel along with all related information including posts from the database. -Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, - Example: " channel delete myteam:mychannel", - RunE: deleteChannelsCmdF, -} - -var ListChannelsCmd = &cobra.Command{ - Use: "list [teams]", - Short: "List all channels on specified teams.", - Long: `List all channels on specified teams. -Archived channels are appended with ' (archived)'.`, - Example: " channel list myteam", - RunE: listChannelsCmdF, -} - -var MoveChannelsCmd = &cobra.Command{ - Use: "move [team] [channels]", - Short: "Moves channels to the specified team", - Long: `Moves the provided channels to the specified team. -Validates that all users in the channel belong to the target team. Incoming/Outgoing webhooks are moved along with the channel. -Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, - Example: " channel move newteam oldteam:mychannel", - RunE: moveChannelsCmdF, -} - -var RestoreChannelsCmd = &cobra.Command{ - Use: "restore [channels]", - Short: "Restore some channels", - Long: `Restore a previously deleted channel -Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, - Example: " channel restore myteam:mychannel", - RunE: restoreChannelsCmdF, -} - -var ModifyChannelCmd = &cobra.Command{ - Use: "modify [channel]", - Short: "Modify a channel's public/private type", - Long: `Change the public/private type of a channel. -Channel can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, - Example: " channel modify myteam:mychannel --private", - RunE: modifyChannelCmdF, -} - -func init() { - ChannelCreateCmd.Flags().String("name", "", "Channel Name") - ChannelCreateCmd.Flags().String("display_name", "", "Channel Display Name") - ChannelCreateCmd.Flags().String("team", "", "Team name or ID") - ChannelCreateCmd.Flags().String("header", "", "Channel header") - ChannelCreateCmd.Flags().String("purpose", "", "Channel purpose") - ChannelCreateCmd.Flags().Bool("private", false, "Create a private channel.") - - MoveChannelsCmd.Flags().String("username", "", "Required. Username who is moving the channel.") - - DeleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channels.") - - ModifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel") - ModifyChannelCmd.Flags().Bool("public", false, "Convert the channel to a public channel") - ModifyChannelCmd.Flags().String("username", "", "Required. Username who changes the channel privacy.") - - ChannelCmd.AddCommand( - ChannelCreateCmd, - RemoveChannelUsersCmd, - AddChannelUsersCmd, - ArchiveChannelsCmd, - DeleteChannelsCmd, - ListChannelsCmd, - MoveChannelsCmd, - RestoreChannelsCmd, - ModifyChannelCmd, - ) - - cmd.RootCmd.AddCommand(ChannelCmd) -} - -func createChannelCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - name, errn := command.Flags().GetString("name") - if errn != nil || name == "" { - return errors.New("Name is required") - } - displayname, errdn := command.Flags().GetString("display_name") - if errdn != nil || displayname == "" { - return errors.New("Display Name is required") - } - teamArg, errteam := command.Flags().GetString("team") - if errteam != nil || teamArg == "" { - return errors.New("Team is required") - } - header, _ := command.Flags().GetString("header") - purpose, _ := command.Flags().GetString("purpose") - useprivate, _ := command.Flags().GetBool("private") - - channelType := model.CHANNEL_OPEN - if useprivate { - channelType = model.CHANNEL_PRIVATE - } - - team := getTeamFromTeamArg(a, teamArg) - if team == nil { - return errors.New("Unable to find team: " + teamArg) - } - - channel := &model.Channel{ - TeamId: team.Id, - Name: name, - DisplayName: displayname, - Header: header, - Purpose: purpose, - Type: channelType, - CreatorId: "", - } - - if _, err := a.CreateChannel(channel, false); err != nil { - return err - } - - return nil -} - -func removeChannelUsersCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - - channel := getChannelFromChannelArg(a, args[0]) - if channel == nil { - return errors.New("Unable to find channel '" + args[0] + "'") - } - - users := getUsersFromUserArgs(a, args[1:]) - for i, user := range users { - removeUserFromChannel(a, channel, user, args[i+1]) - } - - return nil -} - -func removeUserFromChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) { - if user == nil { - cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") - return - } - if err := a.RemoveUserFromChannel(user.Id, "", channel); err != nil { - cmd.CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) - } -} - -func addChannelUsersCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - - channel := getChannelFromChannelArg(a, args[0]) - if channel == nil { - return errors.New("Unable to find channel '" + args[0] + "'") - } - - users := getUsersFromUserArgs(a, args[1:]) - for i, user := range users { - addUserToChannel(a, channel, user, args[i+1]) - } - - return nil -} - -func addUserToChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) { - if user == nil { - cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") - return - } - if _, err := a.AddUserToChannel(user, channel); err != nil { - cmd.CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) - } -} - -func archiveChannelsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Enter at least one channel to archive.") - } - - channels := getChannelsFromChannelArgs(a, args) - for i, channel := range channels { - if channel == nil { - cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") - continue - } - if result := <-a.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil { - cmd.CommandPrintErrorln("Unable to archive channel '" + channel.Name + "' error: " + result.Err.Error()) - } - } - - return nil -} - -func deleteChannelsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Enter at least one channel to delete.") - } - - confirmFlag, _ := command.Flags().GetBool("confirm") - if !confirmFlag { - var confirm string - cmd.CommandPrettyPrintln("Are you sure you want to delete the channels specified? All data will be permanently deleted? (YES/NO): ") - fmt.Scanln(&confirm) - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - channels := getChannelsFromChannelArgs(a, args) - for i, channel := range channels { - if channel == nil { - cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") - continue - } - if err := deleteChannel(a, channel); err != nil { - cmd.CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + err.Error()) - } else { - cmd.CommandPrettyPrintln("Deleted channel '" + channel.Name + "'") - } - } - - return nil -} - -func deleteChannel(a *app.App, channel *model.Channel) *model.AppError { - return a.PermanentDeleteChannel(channel) -} - -func moveChannelsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 2 { - return errors.New("Enter the destination team and at least one channel to move.") - } - - team := getTeamFromTeamArg(a, args[0]) - if team == nil { - return errors.New("Unable to find destination team '" + args[0] + "'") - } - - username, erru := command.Flags().GetString("username") - if erru != nil || username == "" { - return errors.New("Username is required") - } - user := getUserFromUserArg(a, username) - - channels := getChannelsFromChannelArgs(a, args[1:]) - for i, channel := range channels { - if channel == nil { - cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") - continue - } - originTeamID := channel.TeamId - if err := moveChannel(a, team, channel, user); err != nil { - cmd.CommandPrintErrorln("Unable to move channel '" + channel.Name + "' error: " + err.Error()) - } else { - cmd.CommandPrettyPrintln("Moved channel '" + channel.Name + "' to " + team.Name + "(" + team.Id + ") from " + originTeamID + ".") - } - } - - return nil -} - -func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *model.User) *model.AppError { - oldTeamId := channel.TeamId - - if err := a.MoveChannel(team, channel, user); err != nil { - return err - } - - if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(oldTeamId, 0, 10000000); err != nil { - return err - } else { - for _, webhook := range incomingWebhooks { - if webhook.ChannelId == channel.Id { - webhook.TeamId = team.Id - if result := <-a.Srv.Store.Webhook().UpdateIncoming(webhook); result.Err != nil { - cmd.CommandPrintErrorln("Failed to move incoming webhook '" + webhook.Id + "' to new team.") - } - } - } - } - - if outgoingWebhooks, err := a.GetOutgoingWebhooksForTeamPage(oldTeamId, 0, 10000000); err != nil { - return err - } else { - for _, webhook := range outgoingWebhooks { - if webhook.ChannelId == channel.Id { - webhook.TeamId = team.Id - if result := <-a.Srv.Store.Webhook().UpdateOutgoing(webhook); result.Err != nil { - cmd.CommandPrintErrorln("Failed to move outgoing webhook '" + webhook.Id + "' to new team.") - } - } - } - } - - return nil -} - -func listChannelsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Enter at least one team.") - } - - teams := getTeamsFromTeamArgs(a, args) - for i, team := range teams { - if team == nil { - cmd.CommandPrintErrorln("Unable to find team '" + args[i] + "'") - continue - } - if result := <-a.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { - cmd.CommandPrintErrorln("Unable to list channels for '" + args[i] + "'") - } else { - channels := result.Data.([]*model.Channel) - - for _, channel := range channels { - if channel.DeleteAt > 0 { - cmd.CommandPrettyPrintln(channel.Name + " (archived)") - } else { - cmd.CommandPrettyPrintln(channel.Name) - } - } - } - } - - return nil -} - -func restoreChannelsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Enter at least one channel.") - } - - channels := getChannelsFromChannelArgs(a, args) - for i, channel := range channels { - if channel == nil { - cmd.CommandPrintErrorln("Unable to find channel '" + args[i] + "'") - continue - } - if result := <-a.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { - cmd.CommandPrintErrorln("Unable to restore channel '" + args[i] + "'") - } - } - - return nil -} - -func modifyChannelCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) != 1 { - return errors.New("Enter at one channel to modify.") - } - - username, erru := command.Flags().GetString("username") - if erru != nil || username == "" { - return errors.New("Username is required") - } - - public, _ := command.Flags().GetBool("public") - private, _ := command.Flags().GetBool("private") - - if public == private { - return errors.New("You must specify only one of --public or --private") - } - - channel := getChannelFromChannelArg(a, args[0]) - if channel == nil { - return errors.New("Unable to find channel '" + args[0] + "'") - } - - if !(channel.Type == model.CHANNEL_OPEN || channel.Type == model.CHANNEL_PRIVATE) { - return errors.New("You can only change the type of public/private channels.") - } - - channel.Type = model.CHANNEL_OPEN - if private { - channel.Type = model.CHANNEL_PRIVATE - } - - user := getUserFromUserArg(a, username) - if _, err := a.UpdateChannelPrivacy(channel, user); err != nil { - return errors.New("Failed to update channel ('" + args[0] + "') privacy - " + err.Error()) - } - - return nil -} diff --git a/cmd/commands/channel_test.go b/cmd/commands/channel_test.go deleted file mode 100644 index 09747b10b..000000000 --- a/cmd/commands/channel_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "strings" - "testing" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/stretchr/testify/require" -) - -func TestJoinChannel(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - channel := th.CreatePublicChannel() - - cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) - - // Joining twice should succeed - cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) - - // should fail because channel does not exist - require.Error(t, cmd.RunCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name+"asdf", th.BasicUser2.Email)) -} - -func TestRemoveChannel(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - channel := th.CreatePublicChannel() - - cmd.CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) - - // should fail because channel does not exist - require.Error(t, cmd.RunCommand(t, "channel", "remove", th.BasicTeam.Name+":doesnotexist", th.BasicUser2.Email)) - - cmd.CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) - - // Leaving twice should succeed - cmd.CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) -} - -func TestMoveChannel(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - team1 := th.BasicTeam - team2 := th.CreateTeam() - user1 := th.BasicUser - th.LinkUserToTeam(user1, team2) - channel := th.BasicChannel - - th.LinkUserToTeam(user1, team1) - th.LinkUserToTeam(user1, team2) - - adminEmail := user1.Email - adminUsername := user1.Username - origin := team1.Name + ":" + channel.Name - dest := team2.Name - - cmd.CheckCommand(t, "channel", "add", origin, adminEmail) - - // should fail with nill because errors are logged instead of returned when a channel does not exist - require.Nil(t, cmd.RunCommand(t, "channel", "move", dest, team1.Name+":doesnotexist", "--username", adminUsername)) - - cmd.CheckCommand(t, "channel", "move", dest, origin, "--username", adminUsername) -} - -func TestListChannels(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - channel := th.CreatePublicChannel() - th.Client.Must(th.Client.DeleteChannel(channel.Id)) - - output := cmd.CheckCommand(t, "channel", "list", th.BasicTeam.Name) - - if !strings.Contains(string(output), "town-square") { - t.Fatal("should have channels") - } - - if !strings.Contains(string(output), channel.Name+" (archived)") { - t.Fatal("should have archived channel") - } -} - -func TestRestoreChannel(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - channel := th.CreatePublicChannel() - th.Client.Must(th.Client.DeleteChannel(channel.Id)) - - cmd.CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) - - // restoring twice should succeed - cmd.CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) -} - -func TestCreateChannel(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - id := model.NewId() - name := "name" + id - - cmd.CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--name", name) - - name = name + "-private" - cmd.CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--private", "--name", name) -} diff --git a/cmd/commands/channelargs.go b/cmd/commands/channelargs.go deleted file mode 100644 index 680fed34b..000000000 --- a/cmd/commands/channelargs.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "fmt" - "strings" - - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/model" -) - -const CHANNEL_ARG_SEPARATOR = ":" - -func getChannelsFromChannelArgs(a *app.App, channelArgs []string) []*model.Channel { - channels := make([]*model.Channel, 0, len(channelArgs)) - for _, channelArg := range channelArgs { - channel := getChannelFromChannelArg(a, channelArg) - channels = append(channels, channel) - } - return channels -} - -func parseChannelArg(channelArg string) (string, string) { - result := strings.SplitN(channelArg, CHANNEL_ARG_SEPARATOR, 2) - if len(result) == 1 { - return "", channelArg - } - return result[0], result[1] -} - -func getChannelFromChannelArg(a *app.App, channelArg string) *model.Channel { - teamArg, channelPart := parseChannelArg(channelArg) - if teamArg == "" && channelPart == "" { - return nil - } - - var channel *model.Channel - if teamArg != "" { - team := getTeamFromTeamArg(a, teamArg) - if team == nil { - return nil - } - - if result := <-a.Srv.Store.Channel().GetByNameIncludeDeleted(team.Id, channelPart, true); result.Err == nil { - channel = result.Data.(*model.Channel) - } else { - fmt.Println(result.Err.Error()) - } - } - - if channel == nil { - if result := <-a.Srv.Store.Channel().Get(channelPart, true); result.Err == nil { - channel = result.Data.(*model.Channel) - } - } - - return channel -} diff --git a/cmd/commands/command.go b/cmd/commands/command.go deleted file mode 100644 index e7b7e0a0d..000000000 --- a/cmd/commands/command.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/spf13/cobra" -) - -var CommandCmd = &cobra.Command{ - Use: "command", - Short: "Management of slash commands", -} - -var CommandMoveCmd = &cobra.Command{ - Use: "move", - Short: "Move a slash command to a different team", - Long: `Move a slash command to a different team. Commands can be specified by [team]:[command-trigger-word]. ie. myteam:trigger or by command ID.`, - Example: ` command move newteam oldteam:command`, - RunE: moveCommandCmdF, -} - -func init() { - CommandCmd.AddCommand( - CommandMoveCmd, - ) - cmd.RootCmd.AddCommand(CommandCmd) -} - -func moveCommandCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 2 { - return errors.New("Enter the destination team and at least one comamnd to move.") - } - - team := getTeamFromTeamArg(a, args[0]) - if team == nil { - return errors.New("Unable to find destination team '" + args[0] + "'") - } - - commands := getCommandsFromCommandArgs(a, args[1:]) - cmd.CommandPrintErrorln(commands) - for i, command := range commands { - if command == nil { - cmd.CommandPrintErrorln("Unable to find command '" + args[i+1] + "'") - continue - } - if err := moveCommand(a, team, command); err != nil { - cmd.CommandPrintErrorln("Unable to move command '" + command.Trigger + "' error: " + err.Error()) - } else { - cmd.CommandPrettyPrintln("Moved command '" + command.Trigger + "'") - } - } - - return nil -} - -func moveCommand(a *app.App, team *model.Team, command *model.Command) *model.AppError { - return a.MoveCommand(team, command) -} diff --git a/cmd/commands/commandargs.go b/cmd/commands/commandargs.go deleted file mode 100644 index 702f01c9a..000000000 --- a/cmd/commands/commandargs.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "fmt" - "strings" - - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/model" -) - -const COMMAND_ARGS_SEPARATOR = ":" - -func getCommandsFromCommandArgs(a *app.App, commandArgs []string) []*model.Command { - commands := make([]*model.Command, 0, len(commandArgs)) - - for _, commandArg := range commandArgs { - command := getCommandFromCommandArg(a, commandArg) - commands = append(commands, command) - } - - return commands -} - -func parseCommandArg(commandArg string) (string, string) { - result := strings.SplitN(commandArg, COMMAND_ARGS_SEPARATOR, 2) - - if len(result) == 1 { - return "", commandArg - } - - return result[0], result[1] -} - -func getCommandFromCommandArg(a *app.App, commandArg string) *model.Command { - teamArg, commandPart := parseCommandArg(commandArg) - if teamArg == "" && commandPart == "" { - return nil - } - - var command *model.Command - if teamArg != "" { - team := getTeamFromTeamArg(a, teamArg) - if team == nil { - return nil - } - - if result := <-a.Srv.Store.Command().GetByTrigger(team.Id, commandPart); result.Err == nil { - command = result.Data.(*model.Command) - } else { - fmt.Println(result.Err.Error()) - } - } - - if command == nil { - if result := <-a.Srv.Store.Command().Get(commandPart); result.Err == nil { - command = result.Data.(*model.Command) - } - } - - return command -} diff --git a/cmd/commands/config.go b/cmd/commands/config.go deleted file mode 100644 index ef3b0f75e..000000000 --- a/cmd/commands/config.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "encoding/json" - "errors" - "os" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/spf13/cobra" -) - -var ConfigCmd = &cobra.Command{ - Use: "config", - Short: "Configuration", -} - -var ValidateConfigCmd = &cobra.Command{ - Use: "validate", - Short: "Validate config file", - Long: "If the config file is valid, this command will output a success message and have a zero exit code. If it is invalid, this command will output an error and have a non-zero exit code.", - RunE: configValidateCmdF, -} - -func init() { - ConfigCmd.AddCommand( - ValidateConfigCmd, - ) - cmd.RootCmd.AddCommand(ConfigCmd) -} - -func configValidateCmdF(command *cobra.Command, args []string) error { - utils.TranslationsPreInit() - model.AppErrorInit(utils.T) - filePath, err := command.Flags().GetString("config") - if err != nil { - return err - } - - filePath = utils.FindConfigFile(filePath) - - file, err := os.Open(filePath) - if err != nil { - return err - } - - decoder := json.NewDecoder(file) - config := model.Config{} - err = decoder.Decode(&config) - if err != nil { - return err - } - - if _, err := file.Stat(); err != nil { - return err - } - - if err := config.IsValid(); err != nil { - return errors.New(utils.T(err.Id)) - } - - cmd.CommandPrettyPrintln("The document is valid") - return nil -} diff --git a/cmd/commands/config_flag_test.go b/cmd/commands/config_flag_test.go deleted file mode 100644 index 59178b620..000000000 --- a/cmd/commands/config_flag_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "encoding/json" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/utils" -) - -func TestConfigFlag(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - utils.TranslationsPreInit() - config, _, _, err := utils.LoadConfig("config.json") - require.Nil(t, err) - configPath := filepath.Join(dir, "foo.json") - require.NoError(t, ioutil.WriteFile(configPath, []byte(config.ToJson()), 0600)) - - timezones := utils.LoadTimezones("timezones.json") - tzConfigPath := filepath.Join(dir, "timezones.json") - timezoneData, _ := json.Marshal(timezones) - require.NoError(t, ioutil.WriteFile(tzConfigPath, timezoneData, 0600)) - - i18n, ok := utils.FindDir("i18n") - require.True(t, ok) - require.NoError(t, utils.CopyDir(i18n, filepath.Join(dir, "i18n"))) - - prevDir, err := os.Getwd() - require.NoError(t, err) - defer os.Chdir(prevDir) - os.Chdir(dir) - - require.Error(t, cmd.RunCommand(t, "version")) - cmd.CheckCommand(t, "--config", "foo.json", "version") - cmd.CheckCommand(t, "--config", "./foo.json", "version") - cmd.CheckCommand(t, "--config", configPath, "version") -} diff --git a/cmd/commands/config_test.go b/cmd/commands/config_test.go deleted file mode 100644 index 54ddfcb61..000000000 --- a/cmd/commands/config_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" -) - -func TestConfigValidate(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - path := filepath.Join(dir, "config.json") - config := &model.Config{} - config.SetDefaults() - require.NoError(t, ioutil.WriteFile(path, []byte(config.ToJson()), 0600)) - - assert.Error(t, cmd.RunCommand(t, "--config", "foo.json", "config", "validate")) - assert.NoError(t, cmd.RunCommand(t, "--config", path, "config", "validate")) -} diff --git a/cmd/commands/exec_command_test.go b/cmd/commands/exec_command_test.go deleted file mode 100644 index 79e65fe83..000000000 --- a/cmd/commands/exec_command_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "flag" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/cmd" -) - -func TestExecCommand(t *testing.T) { - if filter := flag.Lookup("test.run").Value.String(); filter != "ExecCommand" { - t.Skip("use -run ExecCommand to execute a command via the test executable") - } - cmd.RootCmd.SetArgs(flag.Args()) - require.NoError(t, cmd.RootCmd.Execute()) -} diff --git a/cmd/commands/import.go b/cmd/commands/import.go deleted file mode 100644 index 51fbb8d70..000000000 --- a/cmd/commands/import.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - "os" - - "fmt" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/spf13/cobra" -) - -var ImportCmd = &cobra.Command{ - Use: "import", - Short: "Import data.", -} - -var SlackImportCmd = &cobra.Command{ - Use: "slack [team] [file]", - Short: "Import a team from Slack.", - Long: "Import a team from a Slack export zip file.", - Example: " import slack myteam slack_export.zip", - RunE: slackImportCmdF, -} - -var BulkImportCmd = &cobra.Command{ - Use: "bulk [file]", - Short: "Import bulk data.", - Long: "Import data from a Mattermost Bulk Import File.", - Example: " import bulk bulk_data.json", - RunE: bulkImportCmdF, -} - -func init() { - BulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.") - BulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.") - BulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.") - - ImportCmd.AddCommand( - BulkImportCmd, - SlackImportCmd, - ) - cmd.RootCmd.AddCommand(ImportCmd) -} - -func slackImportCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) != 2 { - return errors.New("Incorrect number of arguments.") - } - - team := getTeamFromTeamArg(a, args[0]) - if team == nil { - return errors.New("Unable to find team '" + args[0] + "'") - } - - fileReader, err := os.Open(args[1]) - if err != nil { - return err - } - defer fileReader.Close() - - fileInfo, err := fileReader.Stat() - if err != nil { - return err - } - - cmd.CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.") - - a.SlackImport(fileReader, fileInfo.Size(), team.Id) - - cmd.CommandPrettyPrintln("Finished Slack Import.") - - return nil -} - -func bulkImportCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - apply, err := command.Flags().GetBool("apply") - if err != nil { - return errors.New("Apply flag error") - } - - validate, err := command.Flags().GetBool("validate") - if err != nil { - return errors.New("Validate flag error") - } - - workers, err := command.Flags().GetInt("workers") - if err != nil { - return errors.New("Workers flag error") - } - - if len(args) != 1 { - return errors.New("Incorrect number of arguments.") - } - - fileReader, err := os.Open(args[0]) - if err != nil { - return err - } - defer fileReader.Close() - - if apply && validate { - cmd.CommandPrettyPrintln("Use only one of --apply or --validate.") - return nil - } else if apply && !validate { - cmd.CommandPrettyPrintln("Running Bulk Import. This may take a long time.") - } else { - cmd.CommandPrettyPrintln("Running Bulk Import Data Validation.") - cmd.CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **") - cmd.CommandPrettyPrintln("Use the --apply flag to perform the actual data import.") - } - - cmd.CommandPrettyPrintln("") - - if err, lineNumber := a.BulkImport(fileReader, !apply, workers); err != nil { - cmd.CommandPrettyPrintln(err.Error()) - if lineNumber != 0 { - cmd.CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber)) - } - return err - } else { - if apply { - cmd.CommandPrettyPrintln("Finished Bulk Import.") - } else { - cmd.CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.") - } - } - - return nil -} diff --git a/cmd/commands/jobserver.go b/cmd/commands/jobserver.go deleted file mode 100644 index a7671e190..000000000 --- a/cmd/commands/jobserver.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "os" - "os/signal" - "syscall" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/mlog" - "github.com/spf13/cobra" -) - -var JobserverCmd = &cobra.Command{ - Use: "jobserver", - Short: "Start the Mattermost job server", - Run: jobserverCmdF, -} - -func init() { - JobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.") - JobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.") - - cmd.RootCmd.AddCommand(JobserverCmd) -} - -func jobserverCmdF(command *cobra.Command, args []string) { - // Options - noJobs, _ := command.Flags().GetBool("nojobs") - noSchedule, _ := command.Flags().GetBool("noschedule") - - // Initialize - a, err := cmd.InitDBCommandContext("config.json") - if err != nil { - panic(err.Error()) - } - defer a.Shutdown() - - a.LoadLicense() - - // Run jobs - mlog.Info("Starting Mattermost job server") - if !noJobs { - a.Jobs.StartWorkers() - } - if !noSchedule { - a.Jobs.StartSchedulers() - } - - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - <-signalChan - - // Cleanup anything that isn't handled by a defer statement - mlog.Info("Stopping Mattermost job server") - - a.Jobs.StopSchedulers() - a.Jobs.StopWorkers() - - mlog.Info("Stopped Mattermost job server") -} diff --git a/cmd/commands/ldap.go b/cmd/commands/ldap.go deleted file mode 100644 index 03c366213..000000000 --- a/cmd/commands/ldap.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/spf13/cobra" -) - -var LdapCmd = &cobra.Command{ - Use: "ldap", - Short: "LDAP related utilities", -} - -var LdapSyncCmd = &cobra.Command{ - Use: "sync", - Short: "Synchronize now", - Long: "Synchronize all LDAP users now.", - Example: " ldap sync", - RunE: ldapSyncCmdF, -} - -var LdapIdMigrate = &cobra.Command{ - Use: "idmigrate", - Short: "Migrate LDAP IdAttribute to new value", - Long: "Migrate LDAP IdAttribute to new value. Run this utility then change the IdAttribute to the new value.", - Example: " ldap idmigrate objectGUID", - Args: cobra.ExactArgs(1), - RunE: ldapIdMigrateCmdF, -} - -func init() { - LdapCmd.AddCommand( - LdapSyncCmd, - LdapIdMigrate, - ) - cmd.RootCmd.AddCommand(LdapCmd) -} - -func ldapSyncCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if ldapI := a.Ldap; ldapI != nil { - job, err := ldapI.StartSynchronizeJob(true) - if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED { - cmd.CommandPrintErrorln("ERROR: AD/LDAP Synchronization please check the server logs") - } else { - cmd.CommandPrettyPrintln("SUCCESS: AD/LDAP Synchronization Complete") - } - } - - return nil -} - -func ldapIdMigrateCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - toAttribute := args[0] - if ldapI := a.Ldap; ldapI != nil { - if err := ldapI.MigrateIDAttribute(toAttribute); err != nil { - cmd.CommandPrintErrorln("ERROR: AD/LDAP IdAttribute migration failed! Error: " + err.Error()) - } else { - cmd.CommandPrettyPrintln("SUCCESS: AD/LDAP IdAttribute migration complete. You can now change your IdAttribute to: " + toAttribute) - } - } - - return nil -} diff --git a/cmd/commands/license.go b/cmd/commands/license.go deleted file mode 100644 index 61c8e3060..000000000 --- a/cmd/commands/license.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - "io/ioutil" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/spf13/cobra" -) - -var LicenseCmd = &cobra.Command{ - Use: "license", - Short: "Licensing commands", -} - -var UploadLicenseCmd = &cobra.Command{ - Use: "upload [license]", - Short: "Upload a license.", - Long: "Upload a license. Replaces current license.", - Example: " license upload /path/to/license/mylicensefile.mattermost-license", - RunE: uploadLicenseCmdF, -} - -func init() { - LicenseCmd.AddCommand(UploadLicenseCmd) - cmd.RootCmd.AddCommand(LicenseCmd) -} - -func uploadLicenseCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) != 1 { - return errors.New("Enter one license file to upload") - } - - var fileBytes []byte - if fileBytes, err = ioutil.ReadFile(args[0]); err != nil { - return err - } - - if _, err := a.SaveLicense(fileBytes); err != nil { - return err - } - - cmd.CommandPrettyPrintln("Uploaded license file") - - return nil -} diff --git a/cmd/commands/message_export.go b/cmd/commands/message_export.go deleted file mode 100644 index 30d9cb122..000000000 --- a/cmd/commands/message_export.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - - "context" - - "time" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/spf13/cobra" -) - -var MessageExportCmd = &cobra.Command{ - Use: "export", - Short: "Export data from Mattermost", - Long: "Export data from Mattermost in a format suitable for import into a third-party application", - Example: "export --format=actiance --exportFrom=12345", - RunE: messageExportCmdF, -} - -func init() { - MessageExportCmd.Flags().String("format", "actiance", "The format to export data in") - MessageExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.") - MessageExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.") - cmd.RootCmd.AddCommand(MessageExportCmd) -} - -func messageExportCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if !*a.Config().MessageExportSettings.EnableExport { - return errors.New("ERROR: The message export feature is not enabled") - } - - // for now, format is hard-coded to actiance. In time, we'll have to support other formats and inject them into job data - if format, err := command.Flags().GetString("format"); err != nil { - return errors.New("format flag error") - } else if format != "actiance" { - return errors.New("unsupported export format") - } - - startTime, err := command.Flags().GetInt64("exportFrom") - if err != nil { - return errors.New("exportFrom flag error") - } else if startTime < 0 { - return errors.New("exportFrom must be a positive integer") - } - - timeoutSeconds, err := command.Flags().GetInt("timeoutSeconds") - if err != nil { - return errors.New("timeoutSeconds error") - } else if timeoutSeconds < 0 { - return errors.New("timeoutSeconds must be a positive integer") - } - - if messageExportI := a.MessageExport; messageExportI != nil { - ctx := context.Background() - if timeoutSeconds > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Second*time.Duration(timeoutSeconds)) - defer cancel() - } - - job, err := messageExportI.StartSynchronizeJob(ctx, startTime) - if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED { - cmd.CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs") - } else { - cmd.CommandPrettyPrintln("SUCCESS: Message export job complete") - } - } - - return nil -} diff --git a/cmd/commands/message_export_test.go b/cmd/commands/message_export_test.go deleted file mode 100644 index bd0e049d6..000000000 --- a/cmd/commands/message_export_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" -) - -// There are no tests that actually run the Message Export job, because it can take a long time to complete depending -// on the size of the database that the config is pointing to. As such, these tests just ensure that the CLI command -// fails fast if invalid flags are supplied - -func TestMessageExportNotEnabled(t *testing.T) { - configPath := writeTempConfig(t, false) - defer os.RemoveAll(filepath.Dir(configPath)) - - // should fail fast because the feature isn't enabled - require.Error(t, cmd.RunCommand(t, "--config", configPath, "export")) -} - -func TestMessageExportInvalidFormat(t *testing.T) { - configPath := writeTempConfig(t, true) - defer os.RemoveAll(filepath.Dir(configPath)) - - // should fail fast because format isn't supported - require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "not_actiance", "export")) -} - -func TestMessageExportNegativeExportFrom(t *testing.T) { - configPath := writeTempConfig(t, true) - defer os.RemoveAll(filepath.Dir(configPath)) - - // should fail fast because export from must be a valid timestamp - require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "-1", "export")) -} - -func TestMessageExportNegativeTimeoutSeconds(t *testing.T) { - configPath := writeTempConfig(t, true) - defer os.RemoveAll(filepath.Dir(configPath)) - - // should fail fast because timeout seconds must be a positive int - require.Error(t, cmd.RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "0", "--timeoutSeconds", "-1", "export")) -} - -func writeTempConfig(t *testing.T, isMessageExportEnabled bool) string { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - - utils.TranslationsPreInit() - config, _, _, appErr := utils.LoadConfig("config.json") - require.Nil(t, appErr) - config.MessageExportSettings.EnableExport = model.NewBool(isMessageExportEnabled) - configPath := filepath.Join(dir, "foo.json") - require.NoError(t, ioutil.WriteFile(configPath, []byte(config.ToJson()), 0600)) - - return configPath -} diff --git a/cmd/commands/permissions.go b/cmd/commands/permissions.go deleted file mode 100644 index 33c255a31..000000000 --- a/cmd/commands/permissions.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - "fmt" - - "github.com/spf13/cobra" - - "github.com/mattermost/mattermost-server/cmd" -) - -var PermissionsCmd = &cobra.Command{ - Use: "permissions", - Short: "Management of the Permissions system", -} - -var ResetPermissionsCmd = &cobra.Command{ - Use: "reset", - Short: "Reset the permissions system to its default state", - Long: "Reset the permissions system to its default state", - Example: " permissions reset", - RunE: resetPermissionsCmdF, -} - -func init() { - ResetPermissionsCmd.Flags().Bool("confirm", false, "Confirm you really want to reset the permissions system and a database backup has been performed.") - - PermissionsCmd.AddCommand( - ResetPermissionsCmd, - ) - cmd.RootCmd.AddCommand(PermissionsCmd) -} - -func resetPermissionsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - - confirmFlag, _ := command.Flags().GetBool("confirm") - if !confirmFlag { - var confirm string - cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") - fmt.Scanln(&confirm) - - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - cmd.CommandPrettyPrintln("Are you sure you want to reset the permissions system? All data related to the permissions system will be permanently deleted and all users will revert to having the default permissions. (YES/NO): ") - fmt.Scanln(&confirm) - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - if err := a.ResetPermissionsSystem(); err != nil { - return errors.New(err.Error()) - } - - cmd.CommandPrettyPrintln("Permissions system successfully reset") - - return nil -} diff --git a/cmd/commands/reset.go b/cmd/commands/reset.go deleted file mode 100644 index 15222acae..000000000 --- a/cmd/commands/reset.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/spf13/cobra" -) - -var ResetCmd = &cobra.Command{ - Use: "reset", - Short: "Reset the database to initial state", - Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.", - RunE: resetCmdF, -} - -func init() { - ResetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.") - - cmd.RootCmd.AddCommand(ResetCmd) -} - -func resetCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - confirmFlag, _ := command.Flags().GetBool("confirm") - if !confirmFlag { - var confirm string - cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") - fmt.Scanln(&confirm) - - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - cmd.CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ") - fmt.Scanln(&confirm) - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - a.Srv.Store.DropAllTables() - cmd.CommandPrettyPrintln("Database successfully reset") - - return nil -} diff --git a/cmd/commands/roles.go b/cmd/commands/roles.go deleted file mode 100644 index 6d832d82a..000000000 --- a/cmd/commands/roles.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - - "github.com/mattermost/mattermost-server/cmd" - "github.com/spf13/cobra" -) - -var RolesCmd = &cobra.Command{ - Use: "roles", - Short: "Management of user roles", -} - -var MakeSystemAdminCmd = &cobra.Command{ - Use: "system_admin [users]", - Short: "Set a user as system admin", - Long: "Make some users system admins", - Example: " roles system_admin user1", - RunE: makeSystemAdminCmdF, -} - -var MakeMemberCmd = &cobra.Command{ - Use: "member [users]", - Short: "Remove system admin privileges", - Long: "Remove system admin privileges from some users.", - Example: " roles member user1", - RunE: makeMemberCmdF, -} - -func init() { - RolesCmd.AddCommand( - MakeSystemAdminCmd, - MakeMemberCmd, - ) - cmd.RootCmd.AddCommand(RolesCmd) -} - -func makeSystemAdminCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Enter at least one user.") - } - - users := getUsersFromUserArgs(a, args) - for i, user := range users { - if user == nil { - return errors.New("Unable to find user '" + args[i] + "'") - } - - if _, err := a.UpdateUserRoles(user.Id, "system_admin system_user", true); err != nil { - return err - } - } - - return nil -} - -func makeMemberCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Enter at least one user.") - } - - users := getUsersFromUserArgs(a, args) - for i, user := range users { - if user == nil { - return errors.New("Unable to find user '" + args[i] + "'") - } - - if _, err := a.UpdateUserRoles(user.Id, "system_user", true); err != nil { - return err - } - } - - return nil -} diff --git a/cmd/commands/roles_test.go b/cmd/commands/roles_test.go deleted file mode 100644 index b6b3ae10f..000000000 --- a/cmd/commands/roles_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "testing" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" -) - -func TestAssignRole(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - cmd.CheckCommand(t, "roles", "system_admin", th.BasicUser.Email) - - if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil { - t.Fatal() - } else { - user := result.Data.(*model.User) - if user.Roles != "system_admin system_user" { - t.Fatal() - } - } -} diff --git a/cmd/commands/sampledata.go b/cmd/commands/sampledata.go deleted file mode 100644 index 9f4db8616..000000000 --- a/cmd/commands/sampledata.go +++ /dev/null @@ -1,637 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "math/rand" - "os" - "path" - "sort" - "strings" - "time" - - "github.com/icrowley/fake" - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/cmd" - "github.com/spf13/cobra" -) - -var SampleDataCmd = &cobra.Command{ - Use: "sampledata", - Short: "Generate sample data", - RunE: sampleDataCmdF, -} - -func init() { - SampleDataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).") - SampleDataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.") - SampleDataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.") - SampleDataCmd.Flags().IntP("users", "u", 15, "The number of sample users.") - SampleDataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.") - SampleDataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.") - SampleDataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.") - SampleDataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.") - SampleDataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.") - SampleDataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.") - SampleDataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.") - SampleDataCmd.Flags().IntP("workers", "w", 2, "How many workers to run during the import.") - SampleDataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.") - SampleDataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of loading into the database.") - cmd.RootCmd.AddCommand(SampleDataCmd) -} - -func sliceIncludes(vs []string, t string) bool { - for _, v := range vs { - if v == t { - return true - } - } - return false -} - -func randomPastTime(seconds int) int64 { - now := time.Now() - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.FixedZone("UTC", 0)) - return today.Unix() - int64(rand.Intn(seconds*1000)) -} - -func randomEmoji() string { - emojis := []string{"+1", "-1", "heart", "blush"} - return emojis[rand.Intn(len(emojis))] -} - -func randomReaction(users []string, parentCreateAt int64) app.ReactionImportData { - user := users[rand.Intn(len(users))] - emoji := randomEmoji() - date := parentCreateAt + int64(rand.Intn(100000)) - return app.ReactionImportData{ - User: &user, - EmojiName: &emoji, - CreateAt: &date, - } -} - -func randomReply(users []string, parentCreateAt int64) app.ReplyImportData { - user := users[rand.Intn(len(users))] - message := randomMessage(users) - date := parentCreateAt + int64(rand.Intn(100000)) - return app.ReplyImportData{ - User: &user, - Message: &message, - CreateAt: &date, - } -} - -func randomMessage(users []string) string { - var message string - switch rand.Intn(30) { - case 0: - mention := users[rand.Intn(len(users))] - message = "@" + mention + " " + fake.Sentence() - case 1: - switch rand.Intn(2) { - case 0: - mattermostVideos := []string{"Q4MgnxbpZas", "BFo7E9-Kc_E", "LsMLR-BHsKg", "MRmGDhlMhNA", "mUOPxT7VgWc"} - message = "https://www.youtube.com/watch?v=" + mattermostVideos[rand.Intn(len(mattermostVideos))] - case 1: - mattermostTweets := []string{"943119062334353408", "949370809528832005", "948539688171819009", "939122439115681792", "938061722027425797"} - message = "https://twitter.com/mattermosthq/status/" + mattermostTweets[rand.Intn(len(mattermostTweets))] - } - case 2: - message = "" - if rand.Intn(2) == 0 { - message += fake.Sentence() - } - for i := 0; i < rand.Intn(4)+1; i++ { - message += "\n * " + fake.Word() - } - default: - if rand.Intn(2) == 0 { - message = fake.Sentence() - } else { - message = fake.Paragraph() - } - if rand.Intn(3) == 0 { - message += "\n" + fake.Sentence() - } - if rand.Intn(3) == 0 { - message += "\n" + fake.Sentence() - } - if rand.Intn(3) == 0 { - message += "\n" + fake.Sentence() - } - } - return message -} - -func sampleDataCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - seed, err := command.Flags().GetInt64("seed") - if err != nil { - return errors.New("Invalid seed parameter") - } - bulk, err := command.Flags().GetString("bulk") - if err != nil { - return errors.New("Invalid bulk parameter") - } - teams, err := command.Flags().GetInt("teams") - if err != nil || teams < 0 { - return errors.New("Invalid teams parameter") - } - channelsPerTeam, err := command.Flags().GetInt("channels-per-team") - if err != nil || channelsPerTeam < 0 { - return errors.New("Invalid channels-per-team parameter") - } - users, err := command.Flags().GetInt("users") - if err != nil || users < 0 { - return errors.New("Invalid users parameter") - } - teamMemberships, err := command.Flags().GetInt("team-memberships") - if err != nil || teamMemberships < 0 { - return errors.New("Invalid team-memberships parameter") - } - channelMemberships, err := command.Flags().GetInt("channel-memberships") - if err != nil || channelMemberships < 0 { - return errors.New("Invalid channel-memberships parameter") - } - postsPerChannel, err := command.Flags().GetInt("posts-per-channel") - if err != nil || postsPerChannel < 0 { - return errors.New("Invalid posts-per-channel parameter") - } - directChannels, err := command.Flags().GetInt("direct-channels") - if err != nil || directChannels < 0 { - return errors.New("Invalid direct-channels parameter") - } - postsPerDirectChannel, err := command.Flags().GetInt("posts-per-direct-channel") - if err != nil || postsPerDirectChannel < 0 { - return errors.New("Invalid posts-per-direct-channel parameter") - } - groupChannels, err := command.Flags().GetInt("group-channels") - if err != nil || groupChannels < 0 { - return errors.New("Invalid group-channels parameter") - } - postsPerGroupChannel, err := command.Flags().GetInt("posts-per-group-channel") - if err != nil || postsPerGroupChannel < 0 { - return errors.New("Invalid posts-per-group-channel parameter") - } - workers, err := command.Flags().GetInt("workers") - if err != nil { - return errors.New("Invalid workers parameter") - } - profileImagesPath, err := command.Flags().GetString("profile-images") - if err != nil { - return errors.New("Invalid profile-images parameter") - } - profileImages := []string{} - if profileImagesPath != "" { - profileImagesStat, err := os.Stat(profileImagesPath) - if os.IsNotExist(err) { - return errors.New("Profile images folder doesn't exists.") - } - if !profileImagesStat.IsDir() { - return errors.New("profile-images parameters must be a folder path.") - } - profileImagesFiles, err := ioutil.ReadDir(profileImagesPath) - if err != nil { - return errors.New("Invalid profile-images parameter") - } - for _, profileImage := range profileImagesFiles { - profileImages = append(profileImages, path.Join(profileImagesPath, profileImage.Name())) - } - sort.Strings(profileImages) - } - - if workers < 1 { - return errors.New("You must have at least one worker.") - } - if teamMemberships > teams { - return errors.New("You can't have more team memberships than teams.") - } - if channelMemberships > channelsPerTeam { - return errors.New("You can't have more channel memberships than channels per team.") - } - - var bulkFile *os.File - switch bulk { - case "": - bulkFile, err = ioutil.TempFile("", ".mattermost-sample-data-") - defer os.Remove(bulkFile.Name()) - if err != nil { - return errors.New("Unable to open temporary file.") - } - case "-": - bulkFile = os.Stdout - default: - bulkFile, err = os.OpenFile(bulk, os.O_RDWR|os.O_CREATE, 0755) - if err != nil { - return errors.New("Unable to write into the \"" + bulk + "\" file.") - } - } - - encoder := json.NewEncoder(bulkFile) - version := 1 - encoder.Encode(app.LineImportData{Type: "version", Version: &version}) - - fake.Seed(seed) - rand.Seed(seed) - - teamsAndChannels := make(map[string][]string) - for i := 0; i < teams; i++ { - teamLine := createTeam(i) - teamsAndChannels[*teamLine.Team.Name] = []string{} - encoder.Encode(teamLine) - } - - teamsList := []string{} - for teamName := range teamsAndChannels { - teamsList = append(teamsList, teamName) - } - sort.Strings(teamsList) - - for _, teamName := range teamsList { - for i := 0; i < channelsPerTeam; i++ { - channelLine := createChannel(i, teamName) - teamsAndChannels[teamName] = append(teamsAndChannels[teamName], *channelLine.Channel.Name) - encoder.Encode(channelLine) - } - } - - allUsers := []string{} - for i := 0; i < users; i++ { - userLine := createUser(i, teamMemberships, channelMemberships, teamsAndChannels, profileImages) - encoder.Encode(userLine) - allUsers = append(allUsers, *userLine.User.Username) - } - - for team, channels := range teamsAndChannels { - for _, channel := range channels { - for i := 0; i < postsPerChannel; i++ { - postLine := createPost(team, channel, allUsers) - encoder.Encode(postLine) - } - } - } - - for i := 0; i < directChannels; i++ { - user1 := allUsers[rand.Intn(len(allUsers))] - user2 := allUsers[rand.Intn(len(allUsers))] - channelLine := createDirectChannel([]string{user1, user2}) - encoder.Encode(channelLine) - for j := 0; j < postsPerDirectChannel; j++ { - postLine := createDirectPost([]string{user1, user2}) - encoder.Encode(postLine) - } - } - - for i := 0; i < groupChannels; i++ { - users := []string{} - totalUsers := 3 + rand.Intn(3) - for len(users) < totalUsers { - user := allUsers[rand.Intn(len(allUsers))] - if !sliceIncludes(users, user) { - users = append(users, user) - } - } - channelLine := createDirectChannel(users) - encoder.Encode(channelLine) - for j := 0; j < postsPerGroupChannel; j++ { - postLine := createDirectPost(users) - encoder.Encode(postLine) - } - } - - if bulk == "" { - _, err := bulkFile.Seek(0, 0) - if err != nil { - return errors.New("Unable to read correctly the temporary file.") - } - importErr, lineNumber := a.BulkImport(bulkFile, false, workers) - if importErr != nil { - return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber) - } - } else if bulk != "-" { - err := bulkFile.Close() - if err != nil { - return errors.New("Unable to close correctly the output file") - } - } - - return nil -} - -func createUser(idx int, teamMemberships int, channelMemberships int, teamsAndChannels map[string][]string, profileImages []string) app.LineImportData { - password := fmt.Sprintf("user-%d", idx) - email := fmt.Sprintf("user-%d@sample.mattermost.com", idx) - firstName := fake.FirstName() - lastName := fake.LastName() - username := fmt.Sprintf("%s.%s", strings.ToLower(firstName), strings.ToLower(lastName)) - if idx == 0 { - username = "sysadmin" - password = "sysadmin" - email = "sysadmin@sample.mattermost.com" - } else if idx == 1 { - username = "user-1" - } - position := fake.JobTitle() - roles := "system_user" - if idx%5 == 0 { - roles = "system_admin system_user" - } - - // The 75% of the users have custom profile image - var profileImage *string = nil - if rand.Intn(4) != 0 { - profileImageSelector := rand.Int() - if len(profileImages) > 0 { - profileImage = &profileImages[profileImageSelector%len(profileImages)] - } - } - - useMilitaryTime := "false" - if idx != 0 && rand.Intn(2) == 0 { - useMilitaryTime = "true" - } - - collapsePreviews := "false" - if idx != 0 && rand.Intn(2) == 0 { - collapsePreviews = "true" - } - - messageDisplay := "clean" - if idx != 0 && rand.Intn(2) == 0 { - messageDisplay = "compact" - } - - channelDisplayMode := "full" - if idx != 0 && rand.Intn(2) == 0 { - channelDisplayMode = "centered" - } - - // Some users has nickname - nickname := "" - if rand.Intn(5) == 0 { - nickname = fake.Company() - } - - // Half of users skip tutorial - tutorialStep := "999" - switch rand.Intn(6) { - case 1: - tutorialStep = "1" - case 2: - tutorialStep = "2" - case 3: - tutorialStep = "3" - } - - teams := []app.UserTeamImportData{} - possibleTeams := []string{} - for teamName := range teamsAndChannels { - possibleTeams = append(possibleTeams, teamName) - } - sort.Strings(possibleTeams) - for x := 0; x < teamMemberships; x++ { - if len(possibleTeams) == 0 { - break - } - position := rand.Intn(len(possibleTeams)) - team := possibleTeams[position] - possibleTeams = append(possibleTeams[:position], possibleTeams[position+1:]...) - if teamChannels, err := teamsAndChannels[team]; err { - teams = append(teams, createTeamMembership(channelMemberships, teamChannels, &team)) - } - } - - user := app.UserImportData{ - ProfileImage: profileImage, - Username: &username, - Email: &email, - Password: &password, - Nickname: &nickname, - FirstName: &firstName, - LastName: &lastName, - Position: &position, - Roles: &roles, - Teams: &teams, - UseMilitaryTime: &useMilitaryTime, - CollapsePreviews: &collapsePreviews, - MessageDisplay: &messageDisplay, - ChannelDisplayMode: &channelDisplayMode, - TutorialStep: &tutorialStep, - } - return app.LineImportData{ - Type: "user", - User: &user, - } -} - -func createTeamMembership(numOfchannels int, teamChannels []string, teamName *string) app.UserTeamImportData { - roles := "team_user" - if rand.Intn(5) == 0 { - roles = "team_user team_admin" - } - channels := []app.UserChannelImportData{} - teamChannelsCopy := append([]string(nil), teamChannels...) - for x := 0; x < numOfchannels; x++ { - if len(teamChannelsCopy) == 0 { - break - } - position := rand.Intn(len(teamChannelsCopy)) - channelName := teamChannelsCopy[position] - teamChannelsCopy = append(teamChannelsCopy[:position], teamChannelsCopy[position+1:]...) - channels = append(channels, createChannelMembership(channelName)) - } - - return app.UserTeamImportData{ - Name: teamName, - Roles: &roles, - Channels: &channels, - } -} - -func createChannelMembership(channelName string) app.UserChannelImportData { - roles := "channel_user" - if rand.Intn(5) == 0 { - roles = "channel_user channel_admin" - } - favorite := rand.Intn(5) == 0 - - return app.UserChannelImportData{ - Name: &channelName, - Roles: &roles, - Favorite: &favorite, - } -} - -func createTeam(idx int) app.LineImportData { - displayName := fake.Word() - name := fmt.Sprintf("%s-%d", fake.Word(), idx) - allowOpenInvite := rand.Intn(2) == 0 - - description := fake.Paragraph() - if len(description) > 255 { - description = description[0:255] - } - - teamType := "O" - if rand.Intn(2) == 0 { - teamType = "I" - } - - team := app.TeamImportData{ - DisplayName: &displayName, - Name: &name, - AllowOpenInvite: &allowOpenInvite, - Description: &description, - Type: &teamType, - } - return app.LineImportData{ - Type: "team", - Team: &team, - } -} - -func createChannel(idx int, teamName string) app.LineImportData { - displayName := fake.Word() - name := fmt.Sprintf("%s-%d", fake.Word(), idx) - header := fake.Paragraph() - purpose := fake.Paragraph() - - if len(purpose) > 250 { - purpose = purpose[0:250] - } - - channelType := "P" - if rand.Intn(2) == 0 { - channelType = "O" - } - - channel := app.ChannelImportData{ - Team: &teamName, - Name: &name, - DisplayName: &displayName, - Type: &channelType, - Header: &header, - Purpose: &purpose, - } - return app.LineImportData{ - Type: "channel", - Channel: &channel, - } -} - -func createPost(team string, channel string, allUsers []string) app.LineImportData { - message := randomMessage(allUsers) - create_at := randomPastTime(50000) - user := allUsers[rand.Intn(len(allUsers))] - - // Some messages are flagged by an user - flagged_by := []string{} - if rand.Intn(10) == 0 { - flagged_by = append(flagged_by, allUsers[rand.Intn(len(allUsers))]) - } - - reactions := []app.ReactionImportData{} - if rand.Intn(10) == 0 { - for { - reactions = append(reactions, randomReaction(allUsers, create_at)) - if rand.Intn(3) == 0 { - break - } - } - } - - replies := []app.ReplyImportData{} - if rand.Intn(10) == 0 { - for { - replies = append(replies, randomReply(allUsers, create_at)) - if rand.Intn(4) == 0 { - break - } - } - } - - post := app.PostImportData{ - Team: &team, - Channel: &channel, - User: &user, - Message: &message, - CreateAt: &create_at, - FlaggedBy: &flagged_by, - Reactions: &reactions, - Replies: &replies, - } - return app.LineImportData{ - Type: "post", - Post: &post, - } -} - -func createDirectChannel(members []string) app.LineImportData { - header := fake.Sentence() - - channel := app.DirectChannelImportData{ - Members: &members, - Header: &header, - } - return app.LineImportData{ - Type: "direct_channel", - DirectChannel: &channel, - } -} - -func createDirectPost(members []string) app.LineImportData { - message := randomMessage(members) - create_at := randomPastTime(50000) - user := members[rand.Intn(len(members))] - - // Some messages are flagged by an user - flagged_by := []string{} - if rand.Intn(10) == 0 { - flagged_by = append(flagged_by, members[rand.Intn(len(members))]) - } - - reactions := []app.ReactionImportData{} - if rand.Intn(10) == 0 { - for { - reactions = append(reactions, randomReaction(members, create_at)) - if rand.Intn(3) == 0 { - break - } - } - } - - replies := []app.ReplyImportData{} - if rand.Intn(10) == 0 { - for { - replies = append(replies, randomReply(members, create_at)) - if rand.Intn(4) == 0 { - break - } - } - } - - post := app.DirectPostImportData{ - ChannelMembers: &members, - User: &user, - Message: &message, - CreateAt: &create_at, - FlaggedBy: &flagged_by, - Reactions: &reactions, - Replies: &replies, - } - return app.LineImportData{ - Type: "direct_post", - DirectPost: &post, - } -} diff --git a/cmd/commands/sampledata_test.go b/cmd/commands/sampledata_test.go deleted file mode 100644 index 183392f11..000000000 --- a/cmd/commands/sampledata_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "testing" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/cmd" - "github.com/stretchr/testify/require" -) - -func TestSampledataBadParameters(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - // should fail because you need at least 1 worker - require.Error(t, cmd.RunCommand(t, "sampledata", "--workers", "0")) - - // should fail because you have more team memberships than teams - require.Error(t, cmd.RunCommand(t, "sampledata", "--teams", "10", "--teams-memberships", "11")) - - // should fail because you have more channel memberships than channels per team - require.Error(t, cmd.RunCommand(t, "sampledata", "--channels-per-team", "10", "--channel-memberships", "11")) -} diff --git a/cmd/commands/server.go b/cmd/commands/server.go deleted file mode 100644 index 0d354f08e..000000000 --- a/cmd/commands/server.go +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "fmt" - "net" - "os" - "os/signal" - "syscall" - "time" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/manualtesting" - "github.com/mattermost/mattermost-server/mlog" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/mattermost/mattermost-server/web" - "github.com/mattermost/mattermost-server/wsapi" - "github.com/spf13/cobra" -) - -const ( - SESSIONS_CLEANUP_BATCH_SIZE = 1000 -) - -var MaxNotificationsPerChannelDefault int64 = 1000000 - -var serverCmd = &cobra.Command{ - Use: "server", - Short: "Run the Mattermost server", - RunE: serverCmdF, - SilenceUsage: true, -} - -func init() { - cmd.RootCmd.AddCommand(serverCmd) - cmd.RootCmd.RunE = serverCmdF -} - -func serverCmdF(command *cobra.Command, args []string) error { - config, err := command.Flags().GetString("config") - if err != nil { - return err - } - - disableConfigWatch, _ := command.Flags().GetBool("disableconfigwatch") - - interruptChan := make(chan os.Signal, 1) - return runServer(config, disableConfigWatch, interruptChan) -} - -func runServer(configFileLocation string, disableConfigWatch bool, interruptChan chan os.Signal) error { - options := []app.Option{app.ConfigFile(configFileLocation)} - if disableConfigWatch { - options = append(options, app.DisableConfigWatch) - } - - a, err := app.New(options...) - if err != nil { - mlog.Critical(err.Error()) - return err - } - defer a.Shutdown() - - utils.TestConnection(a.Config()) - - pwd, _ := os.Getwd() - mlog.Info(fmt.Sprintf("Current version is %v (%v/%v/%v/%v)", model.CurrentVersion, model.BuildNumber, model.BuildDate, model.BuildHash, model.BuildHashEnterprise)) - mlog.Info(fmt.Sprintf("Enterprise Enabled: %v", model.BuildEnterpriseReady)) - mlog.Info(fmt.Sprintf("Current working directory is %v", pwd)) - mlog.Info(fmt.Sprintf("Loaded config file from %v", utils.FindConfigFile(configFileLocation))) - - backend, appErr := a.FileBackend() - if appErr == nil { - appErr = backend.TestConnection() - } - if appErr != nil { - mlog.Error("Problem with file storage settings: " + appErr.Error()) - } - - if model.BuildEnterpriseReady == "true" { - a.LoadLicense() - } - - a.DoAdvancedPermissionsMigration() - - a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil) - a.AddConfigListener(func(prevCfg, cfg *model.Config) { - if *cfg.PluginSettings.Enable { - a.InitPlugins(*cfg.PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil) - } else { - a.ShutDownPlugins() - } - }) - - serverErr := a.StartServer() - if serverErr != nil { - mlog.Critical(serverErr.Error()) - return serverErr - } - - api := api4.Init(a, a.Srv.Router) - wsapi.Init(a, a.Srv.WebSocketRouter) - web.NewWeb(a, a.Srv.Router) - - license := a.License() - - if license == nil && len(a.Config().SqlSettings.DataSourceReplicas) > 1 { - mlog.Warn("More than 1 read replica functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license.") - a.UpdateConfig(func(cfg *model.Config) { - cfg.SqlSettings.DataSourceReplicas = cfg.SqlSettings.DataSourceReplicas[:1] - }) - } - - if license == nil { - a.UpdateConfig(func(cfg *model.Config) { - cfg.TeamSettings.MaxNotificationsPerChannel = &MaxNotificationsPerChannelDefault - }) - } - - a.ReloadConfig() - - // Enable developer settings if this is a "dev" build - if model.BuildNumber == "dev" { - a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true }) - } - - resetStatuses(a) - - // If we allow testing then listen for manual testing URL hits - if a.Config().ServiceSettings.EnableTesting { - manualtesting.Init(api) - } - - a.EnsureDiagnosticId() - - a.Go(func() { - runSecurityJob(a) - }) - a.Go(func() { - runDiagnosticsJob(a) - }) - a.Go(func() { - runSessionCleanupJob(a) - }) - a.Go(func() { - runTokenCleanupJob(a) - }) - a.Go(func() { - runCommandWebhookCleanupJob(a) - }) - - if complianceI := a.Compliance; complianceI != nil { - complianceI.StartComplianceDailyJob() - } - - if a.Cluster != nil { - a.RegisterAllClusterMessageHandlers() - a.Cluster.StartInterNodeCommunication() - } - - if a.Metrics != nil { - a.Metrics.StartServer() - } - - if a.Elasticsearch != nil { - a.Go(func() { - if err := a.Elasticsearch.Start(); err != nil { - mlog.Error(err.Error()) - } - }) - } - - if *a.Config().JobSettings.RunJobs { - a.Jobs.StartWorkers() - } - if *a.Config().JobSettings.RunScheduler { - a.Jobs.StartSchedulers() - } - - notifyReady() - - // wait for kill signal before attempting to gracefully shutdown - // the running service - signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - <-interruptChan - - if a.Cluster != nil { - a.Cluster.StopInterNodeCommunication() - } - - if a.Metrics != nil { - a.Metrics.StopServer() - } - - a.Jobs.StopSchedulers() - a.Jobs.StopWorkers() - - return nil -} - -func runSecurityJob(a *app.App) { - doSecurity(a) - model.CreateRecurringTask("Security", func() { - doSecurity(a) - }, time.Hour*4) -} - -func runDiagnosticsJob(a *app.App) { - doDiagnostics(a) - model.CreateRecurringTask("Diagnostics", func() { - doDiagnostics(a) - }, time.Hour*24) -} - -func runTokenCleanupJob(a *app.App) { - doTokenCleanup(a) - model.CreateRecurringTask("Token Cleanup", func() { - doTokenCleanup(a) - }, time.Hour*1) -} - -func runCommandWebhookCleanupJob(a *app.App) { - doCommandWebhookCleanup(a) - model.CreateRecurringTask("Command Hook Cleanup", func() { - doCommandWebhookCleanup(a) - }, time.Hour*1) -} - -func runSessionCleanupJob(a *app.App) { - doSessionCleanup(a) - model.CreateRecurringTask("Session Cleanup", func() { - doSessionCleanup(a) - }, time.Hour*24) -} - -func resetStatuses(a *app.App) { - if result := <-a.Srv.Store.Status().ResetAll(); result.Err != nil { - mlog.Error(fmt.Sprint("mattermost.reset_status.error FIXME: NOT FOUND IN TRANSLATIONS FILE", result.Err.Error())) - } -} - -func doSecurity(a *app.App) { - a.DoSecurityUpdateCheck() -} - -func doDiagnostics(a *app.App) { - if *a.Config().LogSettings.EnableDiagnostics { - a.SendDailyDiagnostics() - } -} - -func notifyReady() { - // If the environment vars provide a systemd notification socket, - // notify systemd that the server is ready. - systemdSocket := os.Getenv("NOTIFY_SOCKET") - if systemdSocket != "" { - mlog.Info("Sending systemd READY notification.") - - err := sendSystemdReadyNotification(systemdSocket) - if err != nil { - mlog.Error(err.Error()) - } - } -} - -func sendSystemdReadyNotification(socketPath string) error { - msg := "READY=1" - addr := &net.UnixAddr{ - Name: socketPath, - Net: "unixgram", - } - conn, err := net.DialUnix(addr.Net, nil, addr) - if err != nil { - return err - } - defer conn.Close() - _, err = conn.Write([]byte(msg)) - return err -} - -func doTokenCleanup(a *app.App) { - a.Srv.Store.Token().Cleanup() -} - -func doCommandWebhookCleanup(a *app.App) { - a.Srv.Store.CommandWebhook().Cleanup() -} - -func doSessionCleanup(a *app.App) { - a.Srv.Store.Session().Cleanup(model.GetMillis(), SESSIONS_CLEANUP_BATCH_SIZE) -} diff --git a/cmd/commands/server_test.go b/cmd/commands/server_test.go deleted file mode 100644 index fb7dfdef2..000000000 --- a/cmd/commands/server_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "io/ioutil" - "net" - "os" - "syscall" - "testing" - - "github.com/mattermost/mattermost-server/jobs" - "github.com/mattermost/mattermost-server/utils" - "github.com/stretchr/testify/require" -) - -type ServerTestHelper struct { - configPath string - disableConfigWatch bool - interruptChan chan os.Signal - originalInterval int -} - -func SetupServerTest() *ServerTestHelper { - // Build a channel that will be used by the server to receive system signals… - interruptChan := make(chan os.Signal, 1) - // …and sent it immediately a SIGINT value. - // This will make the server loop stop as soon as it started successfully. - interruptChan <- syscall.SIGINT - - // Let jobs poll for termination every 0.2s (instead of every 15s by default) - // Otherwise we would have to wait the whole polling duration before the test - // terminates. - originalInterval := jobs.DEFAULT_WATCHER_POLLING_INTERVAL - jobs.DEFAULT_WATCHER_POLLING_INTERVAL = 200 - - th := &ServerTestHelper{ - configPath: utils.FindConfigFile("config.json"), - disableConfigWatch: true, - interruptChan: interruptChan, - originalInterval: originalInterval, - } - return th -} - -func (th *ServerTestHelper) TearDownServerTest() { - jobs.DEFAULT_WATCHER_POLLING_INTERVAL = th.originalInterval -} - -func TestRunServerSuccess(t *testing.T) { - th := SetupServerTest() - defer th.TearDownServerTest() - - err := runServer(th.configPath, th.disableConfigWatch, th.interruptChan) - require.NoError(t, err) -} - -func TestRunServerInvalidConfigFile(t *testing.T) { - th := SetupServerTest() - defer th.TearDownServerTest() - - // Start the server with an unreadable config file - unreadableConfigFile, err := ioutil.TempFile("", "mattermost-unreadable-config-file-") - if err != nil { - panic(err) - } - os.Chmod(unreadableConfigFile.Name(), 0200) - defer os.Remove(unreadableConfigFile.Name()) - - err = runServer(unreadableConfigFile.Name(), th.disableConfigWatch, th.interruptChan) - require.Error(t, err) -} - -func TestRunServerSystemdNotification(t *testing.T) { - th := SetupServerTest() - defer th.TearDownServerTest() - - // Get a random temporary filename for using as a mock systemd socket - socketFile, err := ioutil.TempFile("", "mattermost-systemd-mock-socket-") - if err != nil { - panic(err) - } - socketPath := socketFile.Name() - os.Remove(socketPath) - - // Set the socket path in the process environment - originalSocket := os.Getenv("NOTIFY_SOCKET") - os.Setenv("NOTIFY_SOCKET", socketPath) - defer os.Setenv("NOTIFY_SOCKET", originalSocket) - - // Open the socket connection - addr := &net.UnixAddr{ - Name: socketPath, - Net: "unixgram", - } - connection, err := net.ListenUnixgram("unixgram", addr) - if err != nil { - panic(err) - } - defer connection.Close() - defer os.Remove(socketPath) - - // Listen for socket data - socketReader := make(chan string) - go func(ch chan string) { - buffer := make([]byte, 512) - count, err := connection.Read(buffer) - if err != nil { - panic(err) - } - data := buffer[0:count] - ch <- string(data) - }(socketReader) - - // Start and stop the server - err = runServer(th.configPath, th.disableConfigWatch, th.interruptChan) - require.NoError(t, err) - - // Ensure the notification has been sent on the socket and is correct - notification := <-socketReader - require.Equal(t, notification, "READY=1") -} - -func TestRunServerNoSystemd(t *testing.T) { - th := SetupServerTest() - defer th.TearDownServerTest() - - // Temporarily remove any Systemd socket defined in the environment - originalSocket := os.Getenv("NOTIFY_SOCKET") - os.Unsetenv("NOTIFY_SOCKET") - defer os.Setenv("NOTIFY_SOCKET", originalSocket) - - err := runServer(th.configPath, th.disableConfigWatch, th.interruptChan) - require.NoError(t, err) -} diff --git a/cmd/commands/team.go b/cmd/commands/team.go deleted file mode 100644 index 7dcf48847..000000000 --- a/cmd/commands/team.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "errors" - "fmt" - - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/spf13/cobra" -) - -var TeamCmd = &cobra.Command{ - Use: "team", - Short: "Management of teams", -} - -var TeamCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a team", - Long: `Create a team.`, - Example: ` team create --name mynewteam --display_name "My New Team" - team create --name private --display_name "My New Private Team" --private`, - RunE: createTeamCmdF, -} - -var RemoveUsersCmd = &cobra.Command{ - Use: "remove [team] [users]", - Short: "Remove users from team", - Long: "Remove some users from team", - Example: " team remove myteam user@example.com username", - RunE: removeUsersCmdF, -} - -var AddUsersCmd = &cobra.Command{ - Use: "add [team] [users]", - Short: "Add users to team", - Long: "Add some users to team", - Example: " team add myteam user@example.com username", - RunE: addUsersCmdF, -} - -var DeleteTeamsCmd = &cobra.Command{ - Use: "delete [teams]", - Short: "Delete teams", - Long: `Permanently delete some teams. -Permanently deletes a team along with all related information including posts from the database.`, - Example: " team delete myteam", - RunE: deleteTeamsCmdF, -} - -var ListTeamsCmd = &cobra.Command{ - Use: "list", - Short: "List all teams.", - Long: `List all teams on the server.`, - Example: " team list", - RunE: listTeamsCmdF, -} - -func init() { - TeamCreateCmd.Flags().String("name", "", "Team Name") - TeamCreateCmd.Flags().String("display_name", "", "Team Display Name") - TeamCreateCmd.Flags().Bool("private", false, "Create a private team.") - TeamCreateCmd.Flags().String("email", "", "Administrator Email (anyone with this email is automatically a team admin)") - - DeleteTeamsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the team and a DB backup has been performed.") - - TeamCmd.AddCommand( - TeamCreateCmd, - RemoveUsersCmd, - AddUsersCmd, - DeleteTeamsCmd, - ListTeamsCmd, - ) - cmd.RootCmd.AddCommand(TeamCmd) -} - -func createTeamCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - name, errn := command.Flags().GetString("name") - if errn != nil || name == "" { - return errors.New("Name is required") - } - displayname, errdn := command.Flags().GetString("display_name") - if errdn != nil || displayname == "" { - return errors.New("Display Name is required") - } - email, _ := command.Flags().GetString("email") - useprivate, _ := command.Flags().GetBool("private") - - teamType := model.TEAM_OPEN - if useprivate { - teamType = model.TEAM_INVITE - } - - team := &model.Team{ - Name: name, - DisplayName: displayname, - Email: email, - Type: teamType, - } - - if _, err := a.CreateTeam(team); err != nil { - return errors.New("Team creation failed: " + err.Error()) - } - - return nil -} - -func removeUsersCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - - team := getTeamFromTeamArg(a, args[0]) - if team == nil { - return errors.New("Unable to find team '" + args[0] + "'") - } - - users := getUsersFromUserArgs(a, args[1:]) - for i, user := range users { - removeUserFromTeam(a, team, user, args[i+1]) - } - - return nil -} - -func removeUserFromTeam(a *app.App, team *model.Team, user *model.User, userArg string) { - if user == nil { - cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") - return - } - if err := a.LeaveTeam(team, user, ""); err != nil { - cmd.CommandPrintErrorln("Unable to remove '" + userArg + "' from " + team.Name + ". Error: " + err.Error()) - } -} - -func addUsersCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 2 { - return errors.New("Not enough arguments.") - } - - team := getTeamFromTeamArg(a, args[0]) - if team == nil { - return errors.New("Unable to find team '" + args[0] + "'") - } - - users := getUsersFromUserArgs(a, args[1:]) - for i, user := range users { - addUserToTeam(a, team, user, args[i+1]) - } - - return nil -} - -func addUserToTeam(a *app.App, team *model.Team, user *model.User, userArg string) { - if user == nil { - cmd.CommandPrintErrorln("Can't find user '" + userArg + "'") - return - } - if err := a.JoinUserToTeam(team, user, ""); err != nil { - cmd.CommandPrintErrorln("Unable to add '" + userArg + "' to " + team.Name) - } -} - -func deleteTeamsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Not enough arguments.") - } - - confirmFlag, _ := command.Flags().GetBool("confirm") - if !confirmFlag { - var confirm string - cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") - fmt.Scanln(&confirm) - - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - cmd.CommandPrettyPrintln("Are you sure you want to delete the teams specified? All data will be permanently deleted? (YES/NO): ") - fmt.Scanln(&confirm) - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - teams := getTeamsFromTeamArgs(a, args) - for i, team := range teams { - if team == nil { - cmd.CommandPrintErrorln("Unable to find team '" + args[i] + "'") - continue - } - if err := deleteTeam(a, team); err != nil { - cmd.CommandPrintErrorln("Unable to delete team '" + team.Name + "' error: " + err.Error()) - } else { - cmd.CommandPrettyPrintln("Deleted team '" + team.Name + "'") - } - } - - return nil -} - -func deleteTeam(a *app.App, team *model.Team) *model.AppError { - return a.PermanentDeleteTeam(team) -} - -func listTeamsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - teams, err2 := a.GetAllTeams() - if err2 != nil { - return err2 - } - - for _, team := range teams { - cmd.CommandPrettyPrintln(team.Name) - } - - return nil -} diff --git a/cmd/commands/team_test.go b/cmd/commands/team_test.go deleted file mode 100644 index e6bc47a09..000000000 --- a/cmd/commands/team_test.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "strings" - "testing" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" -) - -func TestCreateTeam(t *testing.T) { - th := api4.Setup().InitSystemAdmin() - defer th.TearDown() - - id := model.NewId() - name := "name" + id - displayName := "Name " + id - - cmd.CheckCommand(t, "team", "create", "--name", name, "--display_name", displayName) - - found := th.SystemAdminClient.Must(th.SystemAdminClient.TeamExists(name, "")).(bool) - - if !found { - t.Fatal("Failed to create Team") - } -} - -func TestJoinTeam(t *testing.T) { - th := api4.Setup().InitSystemAdmin().InitBasic() - defer th.TearDown() - - cmd.CheckCommand(t, "team", "add", th.BasicTeam.Name, th.BasicUser.Email) - - profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetUsersInTeam(th.BasicTeam.Id, 0, 1000, "")).([]*model.User) - - found := false - - for _, user := range profiles { - if user.Email == th.BasicUser.Email { - found = true - } - - } - - if !found { - t.Fatal("Failed to create User") - } -} - -func TestLeaveTeam(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - cmd.CheckCommand(t, "team", "remove", th.BasicTeam.Name, th.BasicUser.Email) - - profiles := th.Client.Must(th.Client.GetUsersInTeam(th.BasicTeam.Id, 0, 1000, "")).([]*model.User) - - found := false - - for _, user := range profiles { - if user.Email == th.BasicUser.Email { - found = true - } - - } - - if found { - t.Fatal("profile should not be on team") - } - - if result := <-th.App.Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil { - teamMembers := result.Data.([]*model.TeamMember) - if len(teamMembers) > 0 { - t.Fatal("Shouldn't be in team") - } - } -} - -func TestListTeams(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - id := model.NewId() - name := "name" + id - displayName := "Name " + id - - cmd.CheckCommand(t, "team", "create", "--name", name, "--display_name", displayName) - - output := cmd.CheckCommand(t, "team", "list", th.BasicTeam.Name, th.BasicUser.Email) - - if !strings.Contains(string(output), name) { - t.Fatal("should have the created team") - } -} diff --git a/cmd/commands/teamargs.go b/cmd/commands/teamargs.go deleted file mode 100644 index aa62d52b8..000000000 --- a/cmd/commands/teamargs.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/model" -) - -func getTeamsFromTeamArgs(a *app.App, teamArgs []string) []*model.Team { - teams := make([]*model.Team, 0, len(teamArgs)) - for _, teamArg := range teamArgs { - team := getTeamFromTeamArg(a, teamArg) - teams = append(teams, team) - } - return teams -} - -func getTeamFromTeamArg(a *app.App, teamArg string) *model.Team { - var team *model.Team - if result := <-a.Srv.Store.Team().GetByName(teamArg); result.Err == nil { - team = result.Data.(*model.Team) - } - - if team == nil { - if result := <-a.Srv.Store.Team().Get(teamArg); result.Err == nil { - team = result.Data.(*model.Team) - } - } - - return team -} diff --git a/cmd/commands/test.go b/cmd/commands/test.go deleted file mode 100644 index 837b988cc..000000000 --- a/cmd/commands/test.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "bufio" - "fmt" - "os" - "os/exec" - - "os/signal" - "syscall" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/mattermost/mattermost-server/wsapi" - "github.com/spf13/cobra" -) - -var TestCmd = &cobra.Command{ - Use: "test", - Short: "Testing Commands", - Hidden: true, -} - -var RunWebClientTestsCmd = &cobra.Command{ - Use: "web_client_tests", - Short: "Run the web client tests", - RunE: webClientTestsCmdF, -} - -var RunServerForWebClientTestsCmd = &cobra.Command{ - Use: "web_client_tests_server", - Short: "Run the server configured for running the web client tests against it", - RunE: serverForWebClientTestsCmdF, -} - -func init() { - TestCmd.AddCommand( - RunWebClientTestsCmd, - RunServerForWebClientTestsCmd, - ) - cmd.RootCmd.AddCommand(TestCmd) -} - -func webClientTestsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - utils.InitTranslations(a.Config().LocalizationSettings) - serverErr := a.StartServer() - if serverErr != nil { - return serverErr - } - - api4.Init(a, a.Srv.Router) - wsapi.Init(a, a.Srv.WebSocketRouter) - a.UpdateConfig(setupClientTests) - runWebClientTests() - - return nil -} - -func serverForWebClientTestsCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - utils.InitTranslations(a.Config().LocalizationSettings) - serverErr := a.StartServer() - if serverErr != nil { - return serverErr - } - - api4.Init(a, a.Srv.Router) - wsapi.Init(a, a.Srv.WebSocketRouter) - a.UpdateConfig(setupClientTests) - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - <-c - - return nil -} - -func setupClientTests(cfg *model.Config) { - *cfg.TeamSettings.EnableOpenServer = true - *cfg.ServiceSettings.EnableCommands = false - *cfg.ServiceSettings.EnableOnlyAdminIntegrations = false - *cfg.ServiceSettings.EnableCustomEmoji = true - cfg.ServiceSettings.EnableIncomingWebhooks = false - cfg.ServiceSettings.EnableOutgoingWebhooks = false -} - -func executeTestCommand(command *exec.Cmd) { - cmdOutPipe, err := command.StdoutPipe() - if err != nil { - cmd.CommandPrintErrorln("Failed to run tests") - os.Exit(1) - return - } - - cmdErrOutPipe, err := command.StderrPipe() - if err != nil { - cmd.CommandPrintErrorln("Failed to run tests") - os.Exit(1) - return - } - - cmdOutReader := bufio.NewScanner(cmdOutPipe) - cmdErrOutReader := bufio.NewScanner(cmdErrOutPipe) - go func() { - for cmdOutReader.Scan() { - fmt.Println(cmdOutReader.Text()) - } - }() - - go func() { - for cmdErrOutReader.Scan() { - fmt.Println(cmdErrOutReader.Text()) - } - }() - - if err := command.Run(); err != nil { - cmd.CommandPrintErrorln("Client Tests failed") - os.Exit(1) - return - } -} - -func runWebClientTests() { - if webappDir := os.Getenv("WEBAPP_DIR"); webappDir != "" { - os.Chdir(webappDir) - } else { - os.Chdir("../mattermost-webapp") - } - - cmd := exec.Command("npm", "test") - executeTestCommand(cmd) -} diff --git a/cmd/commands/user.go b/cmd/commands/user.go deleted file mode 100644 index 7397cfb2e..000000000 --- a/cmd/commands/user.go +++ /dev/null @@ -1,716 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/spf13/cobra" -) - -var UserCmd = &cobra.Command{ - Use: "user", - Short: "Management of users", -} - -var UserActivateCmd = &cobra.Command{ - Use: "activate [emails, usernames, userIds]", - Short: "Activate users", - Long: "Activate users that have been deactivated.", - Example: ` user activate user@example.com - user activate username`, - RunE: userActivateCmdF, -} - -var UserDeactivateCmd = &cobra.Command{ - Use: "deactivate [emails, usernames, userIds]", - Short: "Deactivate users", - Long: "Deactivate users. Deactivated users are immediately logged out of all sessions and are unable to log back in.", - Example: ` user deactivate user@example.com - user deactivate username`, - RunE: userDeactivateCmdF, -} - -var UserCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a user", - Long: "Create a user", - Example: ` user create --email user@example.com --username userexample --password Password1`, - RunE: userCreateCmdF, -} - -var UserInviteCmd = &cobra.Command{ - Use: "invite [email] [teams]", - Short: "Send user an email invite to a team.", - Long: `Send user an email invite to a team. -You can invite a user to multiple teams by listing them. -You can specify teams by name or ID.`, - Example: ` user invite user@example.com myteam - user invite user@example.com myteam1 myteam2`, - RunE: userInviteCmdF, -} - -var ResetUserPasswordCmd = &cobra.Command{ - Use: "password [user] [password]", - Short: "Set a user's password", - Long: "Set a user's password", - Example: " user password user@example.com Password1", - RunE: resetUserPasswordCmdF, -} - -var updateUserEmailCmd = &cobra.Command{ - Use: "email [user] [new email]", - Short: "Change email of the user", - Long: "Change email of the user.", - Example: ` user email test user@example.com - user activate username`, - RunE: updateUserEmailCmdF, -} - -var ResetUserMfaCmd = &cobra.Command{ - Use: "resetmfa [users]", - Short: "Turn off MFA", - Long: `Turn off multi-factor authentication for a user. -If MFA enforcement is enabled, the user will be forced to re-enable MFA as soon as they login.`, - Example: " user resetmfa user@example.com", - RunE: resetUserMfaCmdF, -} - -var DeleteUserCmd = &cobra.Command{ - Use: "delete [users]", - Short: "Delete users and all posts", - Long: "Permanently delete user and all related information including posts.", - Example: " user delete user@example.com", - RunE: deleteUserCmdF, -} - -var DeleteAllUsersCmd = &cobra.Command{ - Use: "deleteall", - Short: "Delete all users and all posts", - Long: "Permanently delete all users and all related information including posts.", - Example: " user deleteall", - RunE: deleteAllUsersCommandF, -} - -var MigrateAuthCmd = &cobra.Command{ - Use: "migrate_auth [from_auth] [to_auth] [migration-options]", - Short: "Mass migrate user accounts authentication type", - Long: `Migrates accounts from one authentication provider to another. For example, you can upgrade your authentication provider from email to ldap.`, - Example: " user migrate_auth email saml users.json", - Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 2 { - return errors.New("Auth migration requires at least 2 arguments.") - } - - toAuth := args[1] - - if toAuth != "ldap" && toAuth != "saml" { - return errors.New("Invalid to_auth parameter, must be saml or ldap.") - } - - if toAuth == "ldap" && len(args) != 3 { - return errors.New("Ldap migration requires 3 arguments.") - } - - autoFlag, _ := cmd.Flags().GetBool("auto") - - if toAuth == "saml" && autoFlag { - if len(args) != 2 { - return errors.New("Saml migration requires two arguments when using the --auto flag. See help text for details.") - } - } - - if toAuth == "saml" && !autoFlag { - if len(args) != 3 { - return errors.New("Saml migration requires three arguments when not using the --auto flag. See help text for details.") - } - } - return nil - }, - RunE: migrateAuthCmdF, -} - -var VerifyUserCmd = &cobra.Command{ - Use: "verify [users]", - Short: "Verify email of users", - Long: "Verify the emails of some users.", - Example: " user verify user1", - RunE: verifyUserCmdF, -} - -var SearchUserCmd = &cobra.Command{ - Use: "search [users]", - Short: "Search for users", - Long: "Search for users based on username, email, or user ID.", - Example: " user search user1@mail.com user2@mail.com", - RunE: searchUserCmdF, -} - -func init() { - UserCreateCmd.Flags().String("username", "", "Required. Username for the new user account.") - UserCreateCmd.Flags().String("email", "", "Required. The email address for the new user account.") - UserCreateCmd.Flags().String("password", "", "Required. The password for the new user account.") - UserCreateCmd.Flags().String("nickname", "", "Optional. The nickname for the new user account.") - UserCreateCmd.Flags().String("firstname", "", "Optional. The first name for the new user account.") - UserCreateCmd.Flags().String("lastname", "", "Optional. The last name for the new user account.") - UserCreateCmd.Flags().String("locale", "", "Optional. The locale (ex: en, fr) for the new user account.") - UserCreateCmd.Flags().Bool("system_admin", false, "Optional. If supplied, the new user will be a system administrator. Defaults to false.") - - DeleteUserCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") - - DeleteAllUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") - - MigrateAuthCmd.Flags().Bool("force", false, "Force the migration to occur even if there are duplicates on the LDAP server. Duplicates will not be migrated. (ldap only)") - MigrateAuthCmd.Flags().Bool("auto", false, "Automatically migrate all users. Assumes the usernames and emails are identical between Mattermost and SAML services. (saml only)") - MigrateAuthCmd.Flags().Bool("dryRun", false, "Run a simulation of the migration process without changing the database.") - MigrateAuthCmd.SetUsageTemplate(`Usage: - platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags] - -Examples: -{{.Example}} - -Arguments: - from_auth: - The authentication service to migrate users accounts from. - Supported options: email, gitlab, ldap, saml. - - to_auth: - The authentication service to migrate users to. - Supported options: ldap, saml. - - migration-options: - Migration specific options, full command help for more information. - -Flags: -{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} -`) - MigrateAuthCmd.SetHelpTemplate(`Usage: - platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags] - -Examples: -{{.Example}} - -Arguments: - from_auth: - The authentication service to migrate users accounts from. - Supported options: email, gitlab, ldap, saml. - - to_auth: - The authentication service to migrate users to. - Supported options: ldap, saml. - - migration-options (ldap): - match_field: - The field that is guaranteed to be the same in both authentication services. For example, if the users emails are consistent set to email. - Supported options: email, username. - - migration-options (saml): - users_file: - The path of a json file with the usernames and emails of all users to migrate to SAML. The username and email must be the same that the SAML service provider store. And the email must match with the email in mattermost database. - - Example json content: - { - "usr1@email.com": "usr.one", - "usr2@email.com": "usr.two" - } - -Flags: -{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} -`) - - UserCmd.AddCommand( - UserActivateCmd, - UserDeactivateCmd, - UserCreateCmd, - UserInviteCmd, - ResetUserPasswordCmd, - updateUserEmailCmd, - ResetUserMfaCmd, - DeleteUserCmd, - DeleteAllUsersCmd, - MigrateAuthCmd, - VerifyUserCmd, - SearchUserCmd, - ) - cmd.RootCmd.AddCommand(UserCmd) -} - -func userActivateCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Expected at least one argument. See help text for details.") - } - - changeUsersActiveStatus(a, args, true) - - return nil -} - -func changeUsersActiveStatus(a *app.App, userArgs []string, active bool) { - users := getUsersFromUserArgs(a, userArgs) - for i, user := range users { - err := changeUserActiveStatus(a, user, userArgs[i], active) - - if err != nil { - cmd.CommandPrintErrorln(err.Error()) - } - } -} - -func changeUserActiveStatus(a *app.App, user *model.User, userArg string, activate bool) error { - if user == nil { - return fmt.Errorf("Can't find user '%v'", userArg) - } - if user.IsSSOUser() { - fmt.Println("You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync.") - } - if _, err := a.UpdateActive(user, activate); err != nil { - return fmt.Errorf("Unable to change activation status of user: %v", userArg) - } - - return nil -} - -func userDeactivateCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Expected at least one argument. See help text for details.") - } - - changeUsersActiveStatus(a, args, false) - - return nil -} - -func userCreateCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - username, erru := command.Flags().GetString("username") - if erru != nil || username == "" { - return errors.New("Username is required") - } - email, erre := command.Flags().GetString("email") - if erre != nil || email == "" { - return errors.New("Email is required") - } - password, errp := command.Flags().GetString("password") - if errp != nil || password == "" { - return errors.New("Password is required") - } - nickname, _ := command.Flags().GetString("nickname") - firstname, _ := command.Flags().GetString("firstname") - lastname, _ := command.Flags().GetString("lastname") - locale, _ := command.Flags().GetString("locale") - systemAdmin, _ := command.Flags().GetBool("system_admin") - - user := &model.User{ - Username: username, - Email: email, - Password: password, - Nickname: nickname, - FirstName: firstname, - LastName: lastname, - Locale: locale, - } - - if ruser, err := a.CreateUser(user); err != nil { - return errors.New("Unable to create user. Error: " + err.Error()) - } else if systemAdmin { - a.UpdateUserRoles(ruser.Id, "system_user system_admin", false) - } - - cmd.CommandPrettyPrintln("Created User") - - return nil -} - -func userInviteCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 2 { - return errors.New("Expected at least two arguments. See help text for details.") - } - - email := args[0] - if !model.IsValidEmail(email) { - return errors.New("Invalid email") - } - - teams := getTeamsFromTeamArgs(a, args[1:]) - for i, team := range teams { - err := inviteUser(a, email, team, args[i+1]) - - if err != nil { - cmd.CommandPrintErrorln(err.Error()) - } - } - - return nil -} - -func inviteUser(a *app.App, email string, team *model.Team, teamArg string) error { - invites := []string{email} - if team == nil { - return fmt.Errorf("Can't find team '%v'", teamArg) - } - - a.SendInviteEmails(team, "Administrator", invites, *a.Config().ServiceSettings.SiteURL) - cmd.CommandPrettyPrintln("Invites may or may not have been sent.") - - return nil -} - -func resetUserPasswordCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) != 2 { - return errors.New("Expected two arguments. See help text for details.") - } - - user := getUserFromUserArg(a, args[0]) - if user == nil { - return errors.New("Unable to find user '" + args[0] + "'") - } - password := args[1] - - if result := <-a.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(password)); result.Err != nil { - return result.Err - } - - return nil -} - -func updateUserEmailCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) != 2 { - return errors.New("Expected two arguments. See help text for details.") - } - - newEmail := args[1] - - if !model.IsValidEmail(newEmail) { - return errors.New("Invalid email: '" + newEmail + "'") - } - - if len(args) != 2 { - return errors.New("Expected two arguments. See help text for details.") - } - - user := getUserFromUserArg(a, args[0]) - if user == nil { - return errors.New("Unable to find user '" + args[0] + "'") - } - - user.Email = newEmail - _, errUpdate := a.UpdateUser(user, true) - if errUpdate != nil { - return errors.New(errUpdate.Message) - } - - return nil -} - -func resetUserMfaCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Expected at least one argument. See help text for details.") - } - - users := getUsersFromUserArgs(a, args) - - for i, user := range users { - if user == nil { - return errors.New("Unable to find user '" + args[i] + "'") - } - - if err := a.DeactivateMfa(user.Id); err != nil { - return err - } - } - - return nil -} - -func deleteUserCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Expected at least one argument. See help text for details.") - } - - confirmFlag, _ := command.Flags().GetBool("confirm") - if !confirmFlag { - var confirm string - cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") - fmt.Scanln(&confirm) - - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - cmd.CommandPrettyPrintln("Are you sure you want to permanently delete the specified users? (YES/NO): ") - fmt.Scanln(&confirm) - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - users := getUsersFromUserArgs(a, args) - - for i, user := range users { - if user == nil { - return errors.New("Unable to find user '" + args[i] + "'") - } - - if err := a.PermanentDeleteUser(user); err != nil { - return err - } - } - - return nil -} - -func deleteAllUsersCommandF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) > 0 { - return errors.New("Expected zero arguments.") - } - - confirmFlag, _ := command.Flags().GetBool("confirm") - if !confirmFlag { - var confirm string - cmd.CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") - fmt.Scanln(&confirm) - - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - cmd.CommandPrettyPrintln("Are you sure you want to permanently delete all user accounts? (YES/NO): ") - fmt.Scanln(&confirm) - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - if err := a.PermanentDeleteAllUsers(); err != nil { - return err - } - - cmd.CommandPrettyPrintln("All user accounts successfully deleted.") - - return nil -} - -func migrateAuthCmdF(command *cobra.Command, args []string) error { - if args[1] == "saml" { - return migrateAuthToSamlCmdF(command, args) - } - return migrateAuthToLdapCmdF(command, args) -} - -func migrateAuthToLdapCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - fromAuth := args[0] - matchField := args[2] - - if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "saml") { - return errors.New("Invalid from_auth argument") - } - - // Email auth in Mattermost system is represented by "" - if fromAuth == "email" { - fromAuth = "" - } - - if len(matchField) == 0 || (matchField != "email" && matchField != "username") { - return errors.New("Invalid match_field argument") - } - - forceFlag, _ := command.Flags().GetBool("force") - dryRunFlag, _ := command.Flags().GetBool("dryRun") - - if migrate := a.AccountMigration; migrate != nil { - if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag, dryRunFlag); err != nil { - return errors.New("Error while migrating users: " + err.Error()) - } - - cmd.CommandPrettyPrintln("Successfully migrated accounts.") - } - - return nil -} - -func migrateAuthToSamlCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - dryRunFlag, _ := command.Flags().GetBool("dryRun") - autoFlag, _ := command.Flags().GetBool("auto") - - matchesFile := "" - matches := map[string]string{} - if !autoFlag { - matchesFile = args[2] - - file, e := ioutil.ReadFile(matchesFile) - if e != nil { - return errors.New("Invalid users file.") - } - if json.Unmarshal(file, &matches) != nil { - return errors.New("Invalid users file.") - } - } - - fromAuth := args[0] - - if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "ldap") { - return errors.New("Invalid from_auth argument") - } - - if autoFlag && !dryRunFlag { - var confirm string - cmd.CommandPrettyPrintln("You are about to perform an automatic \"" + fromAuth + " to saml\" migration. This must only be done if your current Mattermost users with " + fromAuth + " auth have the same username and email in your SAML service. Otherwise, provide the usernames and emails from your SAML Service using the \"users file\" without the \"--auto\" option.\n\nDo you want to proceed with automatic migration anyway? (YES/NO):") - fmt.Scanln(&confirm) - - if confirm != "YES" { - return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") - } - } - - // Email auth in Mattermost system is represented by "" - if fromAuth == "email" { - fromAuth = "" - } - - if migrate := a.AccountMigration; migrate != nil { - if err := migrate.MigrateToSaml(fromAuth, matches, autoFlag, dryRunFlag); err != nil { - return errors.New("Error while migrating users: " + err.Error()) - } - - cmd.CommandPrettyPrintln("Successfully migrated accounts.") - } - - return nil -} - -func verifyUserCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Expected at least one argument. See help text for details.") - } - - users := getUsersFromUserArgs(a, args) - - for i, user := range users { - if user == nil { - cmd.CommandPrintErrorln("Unable to find user '" + args[i] + "'") - continue - } - if cresult := <-a.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil { - cmd.CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error()) - } - } - - return nil -} - -func searchUserCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - defer a.Shutdown() - - if len(args) < 1 { - return errors.New("Expected at least one argument. See help text for details.") - } - - users := getUsersFromUserArgs(a, args) - - for i, user := range users { - if i > 0 { - cmd.CommandPrettyPrintln("------------------------------") - } - if user == nil { - cmd.CommandPrintErrorln("Unable to find user '" + args[i] + "'") - continue - } - - cmd.CommandPrettyPrintln("id: " + user.Id) - cmd.CommandPrettyPrintln("username: " + user.Username) - cmd.CommandPrettyPrintln("nickname: " + user.Nickname) - cmd.CommandPrettyPrintln("position: " + user.Position) - cmd.CommandPrettyPrintln("first_name: " + user.FirstName) - cmd.CommandPrettyPrintln("last_name: " + user.LastName) - cmd.CommandPrettyPrintln("email: " + user.Email) - cmd.CommandPrettyPrintln("auth_service: " + user.AuthService) - } - - return nil -} diff --git a/cmd/commands/user_test.go b/cmd/commands/user_test.go deleted file mode 100644 index e51a6150b..000000000 --- a/cmd/commands/user_test.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "testing" - - "github.com/mattermost/mattermost-server/api4" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/stretchr/testify/require" -) - -func TestCreateUserWithTeam(t *testing.T) { - th := api4.Setup().InitBasic().InitSystemAdmin() - defer th.TearDown() - - id := model.NewId() - email := "success+" + id + "@simulator.amazonses.com" - username := "name" + id - - cmd.CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) - - cmd.CheckCommand(t, "team", "add", th.BasicTeam.Id, email) - - profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetUsersInTeam(th.BasicTeam.Id, 0, 1000, "")).([]*model.User) - - found := false - - for _, user := range profiles { - if user.Email == email { - found = true - } - - } - - if !found { - t.Fatal("Failed to create User") - } -} - -func TestCreateUserWithoutTeam(t *testing.T) { - th := api4.Setup() - defer th.TearDown() - - id := model.NewId() - email := "success+" + id + "@simulator.amazonses.com" - username := "name" + id - - cmd.CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) - - if result := <-th.App.Srv.Store.User().GetByEmail(email); result.Err != nil { - t.Fatal() - } else { - user := result.Data.(*model.User) - if user.Email != email { - t.Fatal() - } - } -} - -func TestResetPassword(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - cmd.CheckCommand(t, "user", "password", th.BasicUser.Email, "password2") - - th.Client.Logout() - th.BasicUser.Password = "password2" - th.LoginBasic() -} - -func TestMakeUserActiveAndInactive(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - // first inactivate the user - cmd.CheckCommand(t, "user", "deactivate", th.BasicUser.Email) - - // activate the inactive user - cmd.CheckCommand(t, "user", "activate", th.BasicUser.Email) -} - -func TestChangeUserEmail(t *testing.T) { - th := api4.Setup().InitBasic() - defer th.TearDown() - - newEmail := model.NewId() + "@mattermost-test.com" - - cmd.CheckCommand(t, "user", "email", th.BasicUser.Username, newEmail) - if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err == nil { - t.Fatal("should've updated to the new email") - } - if result := <-th.App.Srv.Store.User().GetByEmail(newEmail); result.Err != nil { - t.Fatal() - } else { - user := result.Data.(*model.User) - if user.Email != newEmail { - t.Fatal("should've updated to the new email") - } - } - - // should fail because using an invalid email - require.Error(t, cmd.RunCommand(t, "user", "email", th.BasicUser.Username, "wrong$email.com")) - - // should fail because missing one parameter - require.Error(t, cmd.RunCommand(t, "user", "email", th.BasicUser.Username)) - - // should fail because missing both parameters - require.Error(t, cmd.RunCommand(t, "user", "email")) - - // should fail because have more than 2 parameters - require.Error(t, cmd.RunCommand(t, "user", "email", th.BasicUser.Username, "new@email.com", "extra!")) - - // should fail because user not found - require.Error(t, cmd.RunCommand(t, "user", "email", "invalidUser", newEmail)) - - // should fail because email already in use - require.Error(t, cmd.RunCommand(t, "user", "email", th.BasicUser.Username, th.BasicUser2.Email)) - -} diff --git a/cmd/commands/userargs.go b/cmd/commands/userargs.go deleted file mode 100644 index ddeed6460..000000000 --- a/cmd/commands/userargs.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/model" -) - -func getUsersFromUserArgs(a *app.App, userArgs []string) []*model.User { - users := make([]*model.User, 0, len(userArgs)) - for _, userArg := range userArgs { - user := getUserFromUserArg(a, userArg) - users = append(users, user) - } - return users -} - -func getUserFromUserArg(a *app.App, userArg string) *model.User { - var user *model.User - if result := <-a.Srv.Store.User().GetByEmail(userArg); result.Err == nil { - user = result.Data.(*model.User) - } - - if user == nil { - if result := <-a.Srv.Store.User().GetByUsername(userArg); result.Err == nil { - user = result.Data.(*model.User) - } - } - - if user == nil { - if result := <-a.Srv.Store.User().Get(userArg); result.Err == nil { - user = result.Data.(*model.User) - } - } - - return user -} diff --git a/cmd/commands/version.go b/cmd/commands/version.go deleted file mode 100644 index eaf6a1a68..000000000 --- a/cmd/commands/version.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/cmd" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/store" - "github.com/mattermost/mattermost-server/store/sqlstore" - "github.com/spf13/cobra" -) - -var VersionCmd = &cobra.Command{ - Use: "version", - Short: "Display version information", - RunE: versionCmdF, -} - -func init() { - cmd.RootCmd.AddCommand(VersionCmd) -} - -func versionCmdF(command *cobra.Command, args []string) error { - a, err := cmd.InitDBCommandContextCobra(command) - if err != nil { - return err - } - - printVersion(a) - - return nil -} - -func printVersion(a *app.App) { - cmd.CommandPrintln("Version: " + model.CurrentVersion) - cmd.CommandPrintln("Build Number: " + model.BuildNumber) - cmd.CommandPrintln("Build Date: " + model.BuildDate) - cmd.CommandPrintln("Build Hash: " + model.BuildHash) - cmd.CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady) - if supplier, ok := a.Srv.Store.(*store.LayeredStore).DatabaseLayer.(*sqlstore.SqlSupplier); ok { - cmd.CommandPrintln("DB Version: " + supplier.GetCurrentSchemaVersion()) - } -} diff --git a/cmd/commands/version_test.go b/cmd/commands/version_test.go deleted file mode 100644 index 24a1389b1..000000000 --- a/cmd/commands/version_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package commands - -import ( - "testing" - - "github.com/mattermost/mattermost-server/cmd" -) - -func TestVersion(t *testing.T) { - cmd.CheckCommand(t, "version") -} diff --git a/cmd/init.go b/cmd/init.go deleted file mode 100644 index e3b4e97e1..000000000 --- a/cmd/init.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package cmd - -import ( - "github.com/mattermost/mattermost-server/app" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" - "github.com/spf13/cobra" -) - -func InitDBCommandContextCobra(cmd *cobra.Command) (*app.App, error) { - config, err := cmd.Flags().GetString("config") - if err != nil { - return nil, err - } - - a, err := InitDBCommandContext(config) - if err != nil { - // Returning an error just prints the usage message, so actually panic - panic(err) - } - - a.DoAdvancedPermissionsMigration() - - return a, nil -} - -func InitDBCommandContext(configFileLocation string) (*app.App, error) { - if err := utils.TranslationsPreInit(); err != nil { - return nil, err - } - model.AppErrorInit(utils.T) - - a, err := app.New(app.ConfigFile(configFileLocation)) - if err != nil { - return nil, err - } - - if model.BuildEnterpriseReady == "true" { - a.LoadLicense() - } - - return a, nil -} diff --git a/cmd/mattermost/commands/channel.go b/cmd/mattermost/commands/channel.go new file mode 100644 index 000000000..80740d85e --- /dev/null +++ b/cmd/mattermost/commands/channel.go @@ -0,0 +1,495 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "fmt" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" + "github.com/spf13/cobra" +) + +var ChannelCmd = &cobra.Command{ + Use: "channel", + Short: "Management of channels", +} + +var ChannelCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a channel", + Long: `Create a channel.`, + Example: ` channel create --team myteam --name mynewchannel --display_name "My New Channel" + channel create --team myteam --name mynewprivatechannel --display_name "My New Private Channel" --private`, + RunE: createChannelCmdF, +} + +var RemoveChannelUsersCmd = &cobra.Command{ + Use: "remove [channel] [users]", + Short: "Remove users from channel", + Long: "Remove some users from channel", + Example: " channel remove myteam:mychannel user@example.com username", + RunE: removeChannelUsersCmdF, +} + +var AddChannelUsersCmd = &cobra.Command{ + Use: "add [channel] [users]", + Short: "Add users to channel", + Long: "Add some users to channel", + Example: " channel add myteam:mychannel user@example.com username", + RunE: addChannelUsersCmdF, +} + +var ArchiveChannelsCmd = &cobra.Command{ + Use: "archive [channels]", + Short: "Archive channels", + Long: `Archive some channels. +Archive a channel along with all related information including posts from the database. +Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, + Example: " channel archive myteam:mychannel", + RunE: archiveChannelsCmdF, +} + +var DeleteChannelsCmd = &cobra.Command{ + Use: "delete [channels]", + Short: "Delete channels", + Long: `Permanently delete some channels. +Permanently deletes a channel along with all related information including posts from the database. +Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, + Example: " channel delete myteam:mychannel", + RunE: deleteChannelsCmdF, +} + +var ListChannelsCmd = &cobra.Command{ + Use: "list [teams]", + Short: "List all channels on specified teams.", + Long: `List all channels on specified teams. +Archived channels are appended with ' (archived)'.`, + Example: " channel list myteam", + RunE: listChannelsCmdF, +} + +var MoveChannelsCmd = &cobra.Command{ + Use: "move [team] [channels]", + Short: "Moves channels to the specified team", + Long: `Moves the provided channels to the specified team. +Validates that all users in the channel belong to the target team. Incoming/Outgoing webhooks are moved along with the channel. +Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, + Example: " channel move newteam oldteam:mychannel", + RunE: moveChannelsCmdF, +} + +var RestoreChannelsCmd = &cobra.Command{ + Use: "restore [channels]", + Short: "Restore some channels", + Long: `Restore a previously deleted channel +Channels can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, + Example: " channel restore myteam:mychannel", + RunE: restoreChannelsCmdF, +} + +var ModifyChannelCmd = &cobra.Command{ + Use: "modify [channel]", + Short: "Modify a channel's public/private type", + Long: `Change the public/private type of a channel. +Channel can be specified by [team]:[channel]. ie. myteam:mychannel or by channel ID.`, + Example: " channel modify myteam:mychannel --private", + RunE: modifyChannelCmdF, +} + +func init() { + ChannelCreateCmd.Flags().String("name", "", "Channel Name") + ChannelCreateCmd.Flags().String("display_name", "", "Channel Display Name") + ChannelCreateCmd.Flags().String("team", "", "Team name or ID") + ChannelCreateCmd.Flags().String("header", "", "Channel header") + ChannelCreateCmd.Flags().String("purpose", "", "Channel purpose") + ChannelCreateCmd.Flags().Bool("private", false, "Create a private channel.") + + MoveChannelsCmd.Flags().String("username", "", "Required. Username who is moving the channel.") + + DeleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channels.") + + ModifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel") + ModifyChannelCmd.Flags().Bool("public", false, "Convert the channel to a public channel") + ModifyChannelCmd.Flags().String("username", "", "Required. Username who changes the channel privacy.") + + ChannelCmd.AddCommand( + ChannelCreateCmd, + RemoveChannelUsersCmd, + AddChannelUsersCmd, + ArchiveChannelsCmd, + DeleteChannelsCmd, + ListChannelsCmd, + MoveChannelsCmd, + RestoreChannelsCmd, + ModifyChannelCmd, + ) + + RootCmd.AddCommand(ChannelCmd) +} + +func createChannelCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + name, errn := command.Flags().GetString("name") + if errn != nil || name == "" { + return errors.New("Name is required") + } + displayname, errdn := command.Flags().GetString("display_name") + if errdn != nil || displayname == "" { + return errors.New("Display Name is required") + } + teamArg, errteam := command.Flags().GetString("team") + if errteam != nil || teamArg == "" { + return errors.New("Team is required") + } + header, _ := command.Flags().GetString("header") + purpose, _ := command.Flags().GetString("purpose") + useprivate, _ := command.Flags().GetBool("private") + + channelType := model.CHANNEL_OPEN + if useprivate { + channelType = model.CHANNEL_PRIVATE + } + + team := getTeamFromTeamArg(a, teamArg) + if team == nil { + return errors.New("Unable to find team: " + teamArg) + } + + channel := &model.Channel{ + TeamId: team.Id, + Name: name, + DisplayName: displayname, + Header: header, + Purpose: purpose, + Type: channelType, + CreatorId: "", + } + + if _, err := a.CreateChannel(channel, false); err != nil { + return err + } + + return nil +} + +func removeChannelUsersCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + channel := getChannelFromChannelArg(a, args[0]) + if channel == nil { + return errors.New("Unable to find channel '" + args[0] + "'") + } + + users := getUsersFromUserArgs(a, args[1:]) + for i, user := range users { + removeUserFromChannel(a, channel, user, args[i+1]) + } + + return nil +} + +func removeUserFromChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if err := a.RemoveUserFromChannel(user.Id, "", channel); err != nil { + CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) + } +} + +func addChannelUsersCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + channel := getChannelFromChannelArg(a, args[0]) + if channel == nil { + return errors.New("Unable to find channel '" + args[0] + "'") + } + + users := getUsersFromUserArgs(a, args[1:]) + for i, user := range users { + addUserToChannel(a, channel, user, args[i+1]) + } + + return nil +} + +func addUserToChannel(a *app.App, channel *model.Channel, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if _, err := a.AddUserToChannel(user, channel); err != nil { + CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) + } +} + +func archiveChannelsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Enter at least one channel to archive.") + } + + channels := getChannelsFromChannelArgs(a, args) + for i, channel := range channels { + if channel == nil { + CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + continue + } + if result := <-a.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil { + CommandPrintErrorln("Unable to archive channel '" + channel.Name + "' error: " + result.Err.Error()) + } + } + + return nil +} + +func deleteChannelsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Enter at least one channel to delete.") + } + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + CommandPrettyPrintln("Are you sure you want to delete the channels specified? All data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + channels := getChannelsFromChannelArgs(a, args) + for i, channel := range channels { + if channel == nil { + CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + continue + } + if err := deleteChannel(a, channel); err != nil { + CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + err.Error()) + } else { + CommandPrettyPrintln("Deleted channel '" + channel.Name + "'") + } + } + + return nil +} + +func deleteChannel(a *app.App, channel *model.Channel) *model.AppError { + return a.PermanentDeleteChannel(channel) +} + +func moveChannelsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 2 { + return errors.New("Enter the destination team and at least one channel to move.") + } + + team := getTeamFromTeamArg(a, args[0]) + if team == nil { + return errors.New("Unable to find destination team '" + args[0] + "'") + } + + username, erru := command.Flags().GetString("username") + if erru != nil || username == "" { + return errors.New("Username is required") + } + user := getUserFromUserArg(a, username) + + channels := getChannelsFromChannelArgs(a, args[1:]) + for i, channel := range channels { + if channel == nil { + CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + continue + } + originTeamID := channel.TeamId + if err := moveChannel(a, team, channel, user); err != nil { + CommandPrintErrorln("Unable to move channel '" + channel.Name + "' error: " + err.Error()) + } else { + CommandPrettyPrintln("Moved channel '" + channel.Name + "' to " + team.Name + "(" + team.Id + ") from " + originTeamID + ".") + } + } + + return nil +} + +func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *model.User) *model.AppError { + oldTeamId := channel.TeamId + + if err := a.MoveChannel(team, channel, user); err != nil { + return err + } + + if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(oldTeamId, 0, 10000000); err != nil { + return err + } else { + for _, webhook := range incomingWebhooks { + if webhook.ChannelId == channel.Id { + webhook.TeamId = team.Id + if result := <-a.Srv.Store.Webhook().UpdateIncoming(webhook); result.Err != nil { + CommandPrintErrorln("Failed to move incoming webhook '" + webhook.Id + "' to new team.") + } + } + } + } + + if outgoingWebhooks, err := a.GetOutgoingWebhooksForTeamPage(oldTeamId, 0, 10000000); err != nil { + return err + } else { + for _, webhook := range outgoingWebhooks { + if webhook.ChannelId == channel.Id { + webhook.TeamId = team.Id + if result := <-a.Srv.Store.Webhook().UpdateOutgoing(webhook); result.Err != nil { + CommandPrintErrorln("Failed to move outgoing webhook '" + webhook.Id + "' to new team.") + } + } + } + } + + return nil +} + +func listChannelsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Enter at least one team.") + } + + teams := getTeamsFromTeamArgs(a, args) + for i, team := range teams { + if team == nil { + CommandPrintErrorln("Unable to find team '" + args[i] + "'") + continue + } + if result := <-a.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { + CommandPrintErrorln("Unable to list channels for '" + args[i] + "'") + } else { + channels := result.Data.([]*model.Channel) + + for _, channel := range channels { + if channel.DeleteAt > 0 { + CommandPrettyPrintln(channel.Name + " (archived)") + } else { + CommandPrettyPrintln(channel.Name) + } + } + } + } + + return nil +} + +func restoreChannelsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Enter at least one channel.") + } + + channels := getChannelsFromChannelArgs(a, args) + for i, channel := range channels { + if channel == nil { + CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + continue + } + if result := <-a.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { + CommandPrintErrorln("Unable to restore channel '" + args[i] + "'") + } + } + + return nil +} + +func modifyChannelCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) != 1 { + return errors.New("Enter at one channel to modify.") + } + + username, erru := command.Flags().GetString("username") + if erru != nil || username == "" { + return errors.New("Username is required") + } + + public, _ := command.Flags().GetBool("public") + private, _ := command.Flags().GetBool("private") + + if public == private { + return errors.New("You must specify only one of --public or --private") + } + + channel := getChannelFromChannelArg(a, args[0]) + if channel == nil { + return errors.New("Unable to find channel '" + args[0] + "'") + } + + if !(channel.Type == model.CHANNEL_OPEN || channel.Type == model.CHANNEL_PRIVATE) { + return errors.New("You can only change the type of public/private channels.") + } + + channel.Type = model.CHANNEL_OPEN + if private { + channel.Type = model.CHANNEL_PRIVATE + } + + user := getUserFromUserArg(a, username) + if _, err := a.UpdateChannelPrivacy(channel, user); err != nil { + return errors.New("Failed to update channel ('" + args[0] + "') privacy - " + err.Error()) + } + + return nil +} diff --git a/cmd/mattermost/commands/channel_test.go b/cmd/mattermost/commands/channel_test.go new file mode 100644 index 000000000..8fec971ca --- /dev/null +++ b/cmd/mattermost/commands/channel_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "strings" + "testing" + + "github.com/mattermost/mattermost-server/api4" + "github.com/mattermost/mattermost-server/model" + "github.com/stretchr/testify/require" +) + +func TestJoinChannel(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + channel := th.CreatePublicChannel() + + CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + + // Joining twice should succeed + CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + + // should fail because channel does not exist + require.Error(t, RunCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name+"asdf", th.BasicUser2.Email)) +} + +func TestRemoveChannel(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + channel := th.CreatePublicChannel() + + CheckCommand(t, "channel", "add", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + + // should fail because channel does not exist + require.Error(t, RunCommand(t, "channel", "remove", th.BasicTeam.Name+":doesnotexist", th.BasicUser2.Email)) + + CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) + + // Leaving twice should succeed + CheckCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email) +} + +func TestMoveChannel(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + team1 := th.BasicTeam + team2 := th.CreateTeam() + user1 := th.BasicUser + th.LinkUserToTeam(user1, team2) + channel := th.BasicChannel + + th.LinkUserToTeam(user1, team1) + th.LinkUserToTeam(user1, team2) + + adminEmail := user1.Email + adminUsername := user1.Username + origin := team1.Name + ":" + channel.Name + dest := team2.Name + + CheckCommand(t, "channel", "add", origin, adminEmail) + + // should fail with nill because errors are logged instead of returned when a channel does not exist + require.Nil(t, RunCommand(t, "channel", "move", dest, team1.Name+":doesnotexist", "--username", adminUsername)) + + CheckCommand(t, "channel", "move", dest, origin, "--username", adminUsername) +} + +func TestListChannels(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + channel := th.CreatePublicChannel() + th.Client.Must(th.Client.DeleteChannel(channel.Id)) + + output := CheckCommand(t, "channel", "list", th.BasicTeam.Name) + + if !strings.Contains(string(output), "town-square") { + t.Fatal("should have channels") + } + + if !strings.Contains(string(output), channel.Name+" (archived)") { + t.Fatal("should have archived channel") + } +} + +func TestRestoreChannel(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + channel := th.CreatePublicChannel() + th.Client.Must(th.Client.DeleteChannel(channel.Id)) + + CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) + + // restoring twice should succeed + CheckCommand(t, "channel", "restore", th.BasicTeam.Name+":"+channel.Name) +} + +func TestCreateChannel(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + id := model.NewId() + name := "name" + id + + CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--name", name) + + name = name + "-private" + CheckCommand(t, "channel", "create", "--display_name", name, "--team", th.BasicTeam.Name, "--private", "--name", name) +} diff --git a/cmd/mattermost/commands/channelargs.go b/cmd/mattermost/commands/channelargs.go new file mode 100644 index 000000000..680fed34b --- /dev/null +++ b/cmd/mattermost/commands/channelargs.go @@ -0,0 +1,60 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "fmt" + "strings" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" +) + +const CHANNEL_ARG_SEPARATOR = ":" + +func getChannelsFromChannelArgs(a *app.App, channelArgs []string) []*model.Channel { + channels := make([]*model.Channel, 0, len(channelArgs)) + for _, channelArg := range channelArgs { + channel := getChannelFromChannelArg(a, channelArg) + channels = append(channels, channel) + } + return channels +} + +func parseChannelArg(channelArg string) (string, string) { + result := strings.SplitN(channelArg, CHANNEL_ARG_SEPARATOR, 2) + if len(result) == 1 { + return "", channelArg + } + return result[0], result[1] +} + +func getChannelFromChannelArg(a *app.App, channelArg string) *model.Channel { + teamArg, channelPart := parseChannelArg(channelArg) + if teamArg == "" && channelPart == "" { + return nil + } + + var channel *model.Channel + if teamArg != "" { + team := getTeamFromTeamArg(a, teamArg) + if team == nil { + return nil + } + + if result := <-a.Srv.Store.Channel().GetByNameIncludeDeleted(team.Id, channelPart, true); result.Err == nil { + channel = result.Data.(*model.Channel) + } else { + fmt.Println(result.Err.Error()) + } + } + + if channel == nil { + if result := <-a.Srv.Store.Channel().Get(channelPart, true); result.Err == nil { + channel = result.Data.(*model.Channel) + } + } + + return channel +} diff --git a/cmd/mattermost/commands/cmdtestlib.go b/cmd/mattermost/commands/cmdtestlib.go new file mode 100644 index 000000000..93dcc9566 --- /dev/null +++ b/cmd/mattermost/commands/cmdtestlib.go @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +var coverprofileCounters map[string]int = make(map[string]int) + +func execArgs(t *testing.T, args []string) []string { + ret := []string{"-test.run", "ExecCommand"} + if coverprofile := flag.Lookup("test.coverprofile").Value.String(); coverprofile != "" { + dir := filepath.Dir(coverprofile) + base := filepath.Base(coverprofile) + baseParts := strings.SplitN(base, ".", 2) + coverprofileCounters[t.Name()] = coverprofileCounters[t.Name()] + 1 + baseParts[0] = fmt.Sprintf("%v-%v-%v", baseParts[0], t.Name(), coverprofileCounters[t.Name()]) + ret = append(ret, "-test.coverprofile", filepath.Join(dir, strings.Join(baseParts, "."))) + } + return append(append(ret, "--", "--disableconfigwatch"), args...) +} + +func CheckCommand(t *testing.T, args ...string) string { + path, err := os.Executable() + require.NoError(t, err) + output, err := exec.Command(path, execArgs(t, args)...).CombinedOutput() + require.NoError(t, err, string(output)) + return strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(string(output)), "PASS")) +} + +func RunCommand(t *testing.T, args ...string) error { + path, err := os.Executable() + require.NoError(t, err) + return exec.Command(path, execArgs(t, args)...).Run() +} diff --git a/cmd/mattermost/commands/command.go b/cmd/mattermost/commands/command.go new file mode 100644 index 000000000..147cd823c --- /dev/null +++ b/cmd/mattermost/commands/command.go @@ -0,0 +1,69 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" + "github.com/spf13/cobra" +) + +var CommandCmd = &cobra.Command{ + Use: "command", + Short: "Management of slash commands", +} + +var CommandMoveCmd = &cobra.Command{ + Use: "move", + Short: "Move a slash command to a different team", + Long: `Move a slash command to a different team. Commands can be specified by [team]:[command-trigger-word]. ie. myteam:trigger or by command ID.`, + Example: ` command move newteam oldteam:command`, + RunE: moveCommandCmdF, +} + +func init() { + CommandCmd.AddCommand( + CommandMoveCmd, + ) + RootCmd.AddCommand(CommandCmd) +} + +func moveCommandCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 2 { + return errors.New("Enter the destination team and at least one comamnd to move.") + } + + team := getTeamFromTeamArg(a, args[0]) + if team == nil { + return errors.New("Unable to find destination team '" + args[0] + "'") + } + + commands := getCommandsFromCommandArgs(a, args[1:]) + CommandPrintErrorln(commands) + for i, command := range commands { + if command == nil { + CommandPrintErrorln("Unable to find command '" + args[i+1] + "'") + continue + } + if err := moveCommand(a, team, command); err != nil { + CommandPrintErrorln("Unable to move command '" + command.Trigger + "' error: " + err.Error()) + } else { + CommandPrettyPrintln("Moved command '" + command.Trigger + "'") + } + } + + return nil +} + +func moveCommand(a *app.App, team *model.Team, command *model.Command) *model.AppError { + return a.MoveCommand(team, command) +} diff --git a/cmd/mattermost/commands/commandargs.go b/cmd/mattermost/commands/commandargs.go new file mode 100644 index 000000000..702f01c9a --- /dev/null +++ b/cmd/mattermost/commands/commandargs.go @@ -0,0 +1,64 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "fmt" + "strings" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" +) + +const COMMAND_ARGS_SEPARATOR = ":" + +func getCommandsFromCommandArgs(a *app.App, commandArgs []string) []*model.Command { + commands := make([]*model.Command, 0, len(commandArgs)) + + for _, commandArg := range commandArgs { + command := getCommandFromCommandArg(a, commandArg) + commands = append(commands, command) + } + + return commands +} + +func parseCommandArg(commandArg string) (string, string) { + result := strings.SplitN(commandArg, COMMAND_ARGS_SEPARATOR, 2) + + if len(result) == 1 { + return "", commandArg + } + + return result[0], result[1] +} + +func getCommandFromCommandArg(a *app.App, commandArg string) *model.Command { + teamArg, commandPart := parseCommandArg(commandArg) + if teamArg == "" && commandPart == "" { + return nil + } + + var command *model.Command + if teamArg != "" { + team := getTeamFromTeamArg(a, teamArg) + if team == nil { + return nil + } + + if result := <-a.Srv.Store.Command().GetByTrigger(team.Id, commandPart); result.Err == nil { + command = result.Data.(*model.Command) + } else { + fmt.Println(result.Err.Error()) + } + } + + if command == nil { + if result := <-a.Srv.Store.Command().Get(commandPart); result.Err == nil { + command = result.Data.(*model.Command) + } + } + + return command +} diff --git a/cmd/mattermost/commands/config.go b/cmd/mattermost/commands/config.go new file mode 100644 index 000000000..81ac765ec --- /dev/null +++ b/cmd/mattermost/commands/config.go @@ -0,0 +1,67 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "encoding/json" + "errors" + "os" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/spf13/cobra" +) + +var ConfigCmd = &cobra.Command{ + Use: "config", + Short: "Configuration", +} + +var ValidateConfigCmd = &cobra.Command{ + Use: "validate", + Short: "Validate config file", + Long: "If the config file is valid, this command will output a success message and have a zero exit code. If it is invalid, this command will output an error and have a non-zero exit code.", + RunE: configValidateCmdF, +} + +func init() { + ConfigCmd.AddCommand( + ValidateConfigCmd, + ) + RootCmd.AddCommand(ConfigCmd) +} + +func configValidateCmdF(command *cobra.Command, args []string) error { + utils.TranslationsPreInit() + model.AppErrorInit(utils.T) + filePath, err := command.Flags().GetString("config") + if err != nil { + return err + } + + filePath = utils.FindConfigFile(filePath) + + file, err := os.Open(filePath) + if err != nil { + return err + } + + decoder := json.NewDecoder(file) + config := model.Config{} + err = decoder.Decode(&config) + if err != nil { + return err + } + + if _, err := file.Stat(); err != nil { + return err + } + + if err := config.IsValid(); err != nil { + return errors.New(utils.T(err.Id)) + } + + CommandPrettyPrintln("The document is valid") + return nil +} diff --git a/cmd/mattermost/commands/config_flag_test.go b/cmd/mattermost/commands/config_flag_test.go new file mode 100644 index 000000000..00a817448 --- /dev/null +++ b/cmd/mattermost/commands/config_flag_test.go @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "encoding/json" + + "github.com/mattermost/mattermost-server/utils" +) + +func TestConfigFlag(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + utils.TranslationsPreInit() + config, _, _, err := utils.LoadConfig("config.json") + require.Nil(t, err) + configPath := filepath.Join(dir, "foo.json") + require.NoError(t, ioutil.WriteFile(configPath, []byte(config.ToJson()), 0600)) + + timezones := utils.LoadTimezones("timezones.json") + tzConfigPath := filepath.Join(dir, "timezones.json") + timezoneData, _ := json.Marshal(timezones) + require.NoError(t, ioutil.WriteFile(tzConfigPath, timezoneData, 0600)) + + i18n, ok := utils.FindDir("i18n") + require.True(t, ok) + require.NoError(t, utils.CopyDir(i18n, filepath.Join(dir, "i18n"))) + + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(dir) + + require.Error(t, RunCommand(t, "version")) + CheckCommand(t, "--config", "foo.json", "version") + CheckCommand(t, "--config", "./foo.json", "version") + CheckCommand(t, "--config", configPath, "version") +} diff --git a/cmd/mattermost/commands/config_test.go b/cmd/mattermost/commands/config_test.go new file mode 100644 index 000000000..fcc35bd02 --- /dev/null +++ b/cmd/mattermost/commands/config_test.go @@ -0,0 +1,30 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" +) + +func TestConfigValidate(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path := filepath.Join(dir, "config.json") + config := &model.Config{} + config.SetDefaults() + require.NoError(t, ioutil.WriteFile(path, []byte(config.ToJson()), 0600)) + + assert.Error(t, RunCommand(t, "--config", "foo.json", "config", "validate")) + assert.NoError(t, RunCommand(t, "--config", path, "config", "validate")) +} diff --git a/cmd/mattermost/commands/exec_command_test.go b/cmd/mattermost/commands/exec_command_test.go new file mode 100644 index 000000000..c1c61f382 --- /dev/null +++ b/cmd/mattermost/commands/exec_command_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExecCommand(t *testing.T) { + if filter := flag.Lookup("test.run").Value.String(); filter != "ExecCommand" { + t.Skip("use -run ExecCommand to execute a command via the test executable") + } + RootCmd.SetArgs(flag.Args()) + require.NoError(t, RootCmd.Execute()) +} diff --git a/cmd/mattermost/commands/import.go b/cmd/mattermost/commands/import.go new file mode 100644 index 000000000..91cfaf997 --- /dev/null +++ b/cmd/mattermost/commands/import.go @@ -0,0 +1,144 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "os" + + "fmt" + + "github.com/spf13/cobra" +) + +var ImportCmd = &cobra.Command{ + Use: "import", + Short: "Import data.", +} + +var SlackImportCmd = &cobra.Command{ + Use: "slack [team] [file]", + Short: "Import a team from Slack.", + Long: "Import a team from a Slack export zip file.", + Example: " import slack myteam slack_export.zip", + RunE: slackImportCmdF, +} + +var BulkImportCmd = &cobra.Command{ + Use: "bulk [file]", + Short: "Import bulk data.", + Long: "Import data from a Mattermost Bulk Import File.", + Example: " import bulk bulk_data.json", + RunE: bulkImportCmdF, +} + +func init() { + BulkImportCmd.Flags().Bool("apply", false, "Save the import data to the database. Use with caution - this cannot be reverted.") + BulkImportCmd.Flags().Bool("validate", false, "Validate the import data without making any changes to the system.") + BulkImportCmd.Flags().Int("workers", 2, "How many workers to run whilst doing the import.") + + ImportCmd.AddCommand( + BulkImportCmd, + SlackImportCmd, + ) + RootCmd.AddCommand(ImportCmd) +} + +func slackImportCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) != 2 { + return errors.New("Incorrect number of arguments.") + } + + team := getTeamFromTeamArg(a, args[0]) + if team == nil { + return errors.New("Unable to find team '" + args[0] + "'") + } + + fileReader, err := os.Open(args[1]) + if err != nil { + return err + } + defer fileReader.Close() + + fileInfo, err := fileReader.Stat() + if err != nil { + return err + } + + CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.") + + a.SlackImport(fileReader, fileInfo.Size(), team.Id) + + CommandPrettyPrintln("Finished Slack Import.") + + return nil +} + +func bulkImportCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + apply, err := command.Flags().GetBool("apply") + if err != nil { + return errors.New("Apply flag error") + } + + validate, err := command.Flags().GetBool("validate") + if err != nil { + return errors.New("Validate flag error") + } + + workers, err := command.Flags().GetInt("workers") + if err != nil { + return errors.New("Workers flag error") + } + + if len(args) != 1 { + return errors.New("Incorrect number of arguments.") + } + + fileReader, err := os.Open(args[0]) + if err != nil { + return err + } + defer fileReader.Close() + + if apply && validate { + CommandPrettyPrintln("Use only one of --apply or --validate.") + return nil + } else if apply && !validate { + CommandPrettyPrintln("Running Bulk Import. This may take a long time.") + } else { + CommandPrettyPrintln("Running Bulk Import Data Validation.") + CommandPrettyPrintln("** This checks the validity of the entities in the data file, but does not persist any changes **") + CommandPrettyPrintln("Use the --apply flag to perform the actual data import.") + } + + CommandPrettyPrintln("") + + if err, lineNumber := a.BulkImport(fileReader, !apply, workers); err != nil { + CommandPrettyPrintln(err.Error()) + if lineNumber != 0 { + CommandPrettyPrintln(fmt.Sprintf("Error occurred on data file line %v", lineNumber)) + } + return err + } else { + if apply { + CommandPrettyPrintln("Finished Bulk Import.") + } else { + CommandPrettyPrintln("Validation complete. You can now perform the import by rerunning this command with the --apply flag.") + } + } + + return nil +} diff --git a/cmd/mattermost/commands/init.go b/cmd/mattermost/commands/init.go new file mode 100644 index 000000000..aea2b1230 --- /dev/null +++ b/cmd/mattermost/commands/init.go @@ -0,0 +1,46 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/spf13/cobra" +) + +func InitDBCommandContextCobra(command *cobra.Command) (*app.App, error) { + config, err := command.Flags().GetString("config") + if err != nil { + return nil, err + } + + a, err := InitDBCommandContext(config) + if err != nil { + // Returning an error just prints the usage message, so actually panic + panic(err) + } + + a.DoAdvancedPermissionsMigration() + + return a, nil +} + +func InitDBCommandContext(configFileLocation string) (*app.App, error) { + if err := utils.TranslationsPreInit(); err != nil { + return nil, err + } + model.AppErrorInit(utils.T) + + a, err := app.New(app.ConfigFile(configFileLocation)) + if err != nil { + return nil, err + } + + if model.BuildEnterpriseReady == "true" { + a.LoadLicense() + } + + return a, nil +} diff --git a/cmd/mattermost/commands/jobserver.go b/cmd/mattermost/commands/jobserver.go new file mode 100644 index 000000000..43a21d61f --- /dev/null +++ b/cmd/mattermost/commands/jobserver.go @@ -0,0 +1,62 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "os" + "os/signal" + "syscall" + + "github.com/mattermost/mattermost-server/mlog" + "github.com/spf13/cobra" +) + +var JobserverCmd = &cobra.Command{ + Use: "jobserver", + Short: "Start the Mattermost job server", + Run: jobserverCmdF, +} + +func init() { + JobserverCmd.Flags().Bool("nojobs", false, "Do not run jobs on this jobserver.") + JobserverCmd.Flags().Bool("noschedule", false, "Do not schedule jobs from this jobserver.") + + RootCmd.AddCommand(JobserverCmd) +} + +func jobserverCmdF(command *cobra.Command, args []string) { + // Options + noJobs, _ := command.Flags().GetBool("nojobs") + noSchedule, _ := command.Flags().GetBool("noschedule") + + // Initialize + a, err := InitDBCommandContext("config.json") + if err != nil { + panic(err.Error()) + } + defer a.Shutdown() + + a.LoadLicense() + + // Run jobs + mlog.Info("Starting Mattermost job server") + if !noJobs { + a.Jobs.StartWorkers() + } + if !noSchedule { + a.Jobs.StartSchedulers() + } + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-signalChan + + // Cleanup anything that isn't handled by a defer statement + mlog.Info("Stopping Mattermost job server") + + a.Jobs.StopSchedulers() + a.Jobs.StopWorkers() + + mlog.Info("Stopped Mattermost job server") +} diff --git a/cmd/mattermost/commands/ldap.go b/cmd/mattermost/commands/ldap.go new file mode 100644 index 000000000..7283df0f2 --- /dev/null +++ b/cmd/mattermost/commands/ldap.go @@ -0,0 +1,77 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "github.com/mattermost/mattermost-server/model" + "github.com/spf13/cobra" +) + +var LdapCmd = &cobra.Command{ + Use: "ldap", + Short: "LDAP related utilities", +} + +var LdapSyncCmd = &cobra.Command{ + Use: "sync", + Short: "Synchronize now", + Long: "Synchronize all LDAP users now.", + Example: " ldap sync", + RunE: ldapSyncCmdF, +} + +var LdapIdMigrate = &cobra.Command{ + Use: "idmigrate", + Short: "Migrate LDAP IdAttribute to new value", + Long: "Migrate LDAP IdAttribute to new value. Run this utility then change the IdAttribute to the new value.", + Example: " ldap idmigrate objectGUID", + Args: cobra.ExactArgs(1), + RunE: ldapIdMigrateCmdF, +} + +func init() { + LdapCmd.AddCommand( + LdapSyncCmd, + LdapIdMigrate, + ) + RootCmd.AddCommand(LdapCmd) +} + +func ldapSyncCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if ldapI := a.Ldap; ldapI != nil { + job, err := ldapI.StartSynchronizeJob(true) + if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED { + CommandPrintErrorln("ERROR: AD/LDAP Synchronization please check the server logs") + } else { + CommandPrettyPrintln("SUCCESS: AD/LDAP Synchronization Complete") + } + } + + return nil +} + +func ldapIdMigrateCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + toAttribute := args[0] + if ldapI := a.Ldap; ldapI != nil { + if err := ldapI.MigrateIDAttribute(toAttribute); err != nil { + CommandPrintErrorln("ERROR: AD/LDAP IdAttribute migration failed! Error: " + err.Error()) + } else { + CommandPrettyPrintln("SUCCESS: AD/LDAP IdAttribute migration complete. You can now change your IdAttribute to: " + toAttribute) + } + } + + return nil +} diff --git a/cmd/mattermost/commands/license.go b/cmd/mattermost/commands/license.go new file mode 100644 index 000000000..1e08814d3 --- /dev/null +++ b/cmd/mattermost/commands/license.go @@ -0,0 +1,54 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "io/ioutil" + + "github.com/spf13/cobra" +) + +var LicenseCmd = &cobra.Command{ + Use: "license", + Short: "Licensing commands", +} + +var UploadLicenseCmd = &cobra.Command{ + Use: "upload [license]", + Short: "Upload a license.", + Long: "Upload a license. Replaces current license.", + Example: " license upload /path/to/license/mylicensefile.mattermost-license", + RunE: uploadLicenseCmdF, +} + +func init() { + LicenseCmd.AddCommand(UploadLicenseCmd) + RootCmd.AddCommand(LicenseCmd) +} + +func uploadLicenseCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) != 1 { + return errors.New("Enter one license file to upload") + } + + var fileBytes []byte + if fileBytes, err = ioutil.ReadFile(args[0]); err != nil { + return err + } + + if _, err := a.SaveLicense(fileBytes); err != nil { + return err + } + + CommandPrettyPrintln("Uploaded license file") + + return nil +} diff --git a/cmd/mattermost/commands/message_export.go b/cmd/mattermost/commands/message_export.go new file mode 100644 index 000000000..41b4fd289 --- /dev/null +++ b/cmd/mattermost/commands/message_export.go @@ -0,0 +1,81 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + + "context" + + "time" + + "github.com/mattermost/mattermost-server/model" + "github.com/spf13/cobra" +) + +var MessageExportCmd = &cobra.Command{ + Use: "export", + Short: "Export data from Mattermost", + Long: "Export data from Mattermost in a format suitable for import into a third-party application", + Example: "export --format=actiance --exportFrom=12345", + RunE: messageExportCmdF, +} + +func init() { + MessageExportCmd.Flags().String("format", "actiance", "The format to export data in") + MessageExportCmd.Flags().Int64("exportFrom", -1, "The timestamp of the earliest post to export, expressed in seconds since the unix epoch.") + MessageExportCmd.Flags().Int("timeoutSeconds", -1, "The maximum number of seconds to wait for the job to complete before timing out.") + RootCmd.AddCommand(MessageExportCmd) +} + +func messageExportCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if !*a.Config().MessageExportSettings.EnableExport { + return errors.New("ERROR: The message export feature is not enabled") + } + + // for now, format is hard-coded to actiance. In time, we'll have to support other formats and inject them into job data + if format, err := command.Flags().GetString("format"); err != nil { + return errors.New("format flag error") + } else if format != "actiance" { + return errors.New("unsupported export format") + } + + startTime, err := command.Flags().GetInt64("exportFrom") + if err != nil { + return errors.New("exportFrom flag error") + } else if startTime < 0 { + return errors.New("exportFrom must be a positive integer") + } + + timeoutSeconds, err := command.Flags().GetInt("timeoutSeconds") + if err != nil { + return errors.New("timeoutSeconds error") + } else if timeoutSeconds < 0 { + return errors.New("timeoutSeconds must be a positive integer") + } + + if messageExportI := a.MessageExport; messageExportI != nil { + ctx := context.Background() + if timeoutSeconds > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Second*time.Duration(timeoutSeconds)) + defer cancel() + } + + job, err := messageExportI.StartSynchronizeJob(ctx, startTime) + if err != nil || job.Status == model.JOB_STATUS_ERROR || job.Status == model.JOB_STATUS_CANCELED { + CommandPrintErrorln("ERROR: Message export job failed. Please check the server logs") + } else { + CommandPrettyPrintln("SUCCESS: Message export job complete") + } + } + + return nil +} diff --git a/cmd/mattermost/commands/message_export_test.go b/cmd/mattermost/commands/message_export_test.go new file mode 100644 index 000000000..7572d8b48 --- /dev/null +++ b/cmd/mattermost/commands/message_export_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" +) + +// There are no tests that actually run the Message Export job, because it can take a long time to complete depending +// on the size of the database that the config is pointing to. As such, these tests just ensure that the CLI command +// fails fast if invalid flags are supplied + +func TestMessageExportNotEnabled(t *testing.T) { + configPath := writeTempConfig(t, false) + defer os.RemoveAll(filepath.Dir(configPath)) + + // should fail fast because the feature isn't enabled + require.Error(t, RunCommand(t, "--config", configPath, "export")) +} + +func TestMessageExportInvalidFormat(t *testing.T) { + configPath := writeTempConfig(t, true) + defer os.RemoveAll(filepath.Dir(configPath)) + + // should fail fast because format isn't supported + require.Error(t, RunCommand(t, "--config", configPath, "--format", "not_actiance", "export")) +} + +func TestMessageExportNegativeExportFrom(t *testing.T) { + configPath := writeTempConfig(t, true) + defer os.RemoveAll(filepath.Dir(configPath)) + + // should fail fast because export from must be a valid timestamp + require.Error(t, RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "-1", "export")) +} + +func TestMessageExportNegativeTimeoutSeconds(t *testing.T) { + configPath := writeTempConfig(t, true) + defer os.RemoveAll(filepath.Dir(configPath)) + + // should fail fast because timeout seconds must be a positive int + require.Error(t, RunCommand(t, "--config", configPath, "--format", "actiance", "--exportFrom", "0", "--timeoutSeconds", "-1", "export")) +} + +func writeTempConfig(t *testing.T, isMessageExportEnabled bool) string { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + + utils.TranslationsPreInit() + config, _, _, appErr := utils.LoadConfig("config.json") + require.Nil(t, appErr) + config.MessageExportSettings.EnableExport = model.NewBool(isMessageExportEnabled) + configPath := filepath.Join(dir, "foo.json") + require.NoError(t, ioutil.WriteFile(configPath, []byte(config.ToJson()), 0600)) + + return configPath +} diff --git a/cmd/mattermost/commands/output.go b/cmd/mattermost/commands/output.go new file mode 100644 index 000000000..e4182b436 --- /dev/null +++ b/cmd/mattermost/commands/output.go @@ -0,0 +1,21 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "fmt" + "os" +) + +func CommandPrintln(a ...interface{}) (int, error) { + return fmt.Println(a...) +} + +func CommandPrintErrorln(a ...interface{}) (int, error) { + return fmt.Fprintln(os.Stderr, a...) +} + +func CommandPrettyPrintln(a ...interface{}) (int, error) { + return fmt.Fprintln(os.Stderr, a...) +} diff --git a/cmd/mattermost/commands/permissions.go b/cmd/mattermost/commands/permissions.go new file mode 100644 index 000000000..52cae0ecb --- /dev/null +++ b/cmd/mattermost/commands/permissions.go @@ -0,0 +1,64 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" +) + +var PermissionsCmd = &cobra.Command{ + Use: "permissions", + Short: "Management of the Permissions system", +} + +var ResetPermissionsCmd = &cobra.Command{ + Use: "reset", + Short: "Reset the permissions system to its default state", + Long: "Reset the permissions system to its default state", + Example: " permissions reset", + RunE: resetPermissionsCmdF, +} + +func init() { + ResetPermissionsCmd.Flags().Bool("confirm", false, "Confirm you really want to reset the permissions system and a database backup has been performed.") + + PermissionsCmd.AddCommand( + ResetPermissionsCmd, + ) + RootCmd.AddCommand(PermissionsCmd) +} + +func resetPermissionsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + CommandPrettyPrintln("Are you sure you want to reset the permissions system? All data related to the permissions system will be permanently deleted and all users will revert to having the default permissions. (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + if err := a.ResetPermissionsSystem(); err != nil { + return errors.New(err.Error()) + } + + CommandPrettyPrintln("Permissions system successfully reset") + + return nil +} diff --git a/cmd/mattermost/commands/reset.go b/cmd/mattermost/commands/reset.go new file mode 100644 index 000000000..9b81ec216 --- /dev/null +++ b/cmd/mattermost/commands/reset.go @@ -0,0 +1,53 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" +) + +var ResetCmd = &cobra.Command{ + Use: "reset", + Short: "Reset the database to initial state", + Long: "Completely erases the database causing the loss of all data. This will reset Mattermost to its initial state.", + RunE: resetCmdF, +} + +func init() { + ResetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.") + + RootCmd.AddCommand(ResetCmd) +} + +func resetCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + CommandPrettyPrintln("Are you sure you want to delete everything? All data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + a.Srv.Store.DropAllTables() + CommandPrettyPrintln("Database successfully reset") + + return nil +} diff --git a/cmd/mattermost/commands/roles.go b/cmd/mattermost/commands/roles.go new file mode 100644 index 000000000..b8fdcdba2 --- /dev/null +++ b/cmd/mattermost/commands/roles.go @@ -0,0 +1,89 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + + "github.com/spf13/cobra" +) + +var RolesCmd = &cobra.Command{ + Use: "roles", + Short: "Management of user roles", +} + +var MakeSystemAdminCmd = &cobra.Command{ + Use: "system_admin [users]", + Short: "Set a user as system admin", + Long: "Make some users system admins", + Example: " roles system_admin user1", + RunE: makeSystemAdminCmdF, +} + +var MakeMemberCmd = &cobra.Command{ + Use: "member [users]", + Short: "Remove system admin privileges", + Long: "Remove system admin privileges from some users.", + Example: " roles member user1", + RunE: makeMemberCmdF, +} + +func init() { + RolesCmd.AddCommand( + MakeSystemAdminCmd, + MakeMemberCmd, + ) + RootCmd.AddCommand(RolesCmd) +} + +func makeSystemAdminCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Enter at least one user.") + } + + users := getUsersFromUserArgs(a, args) + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if _, err := a.UpdateUserRoles(user.Id, "system_admin system_user", true); err != nil { + return err + } + } + + return nil +} + +func makeMemberCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Enter at least one user.") + } + + users := getUsersFromUserArgs(a, args) + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if _, err := a.UpdateUserRoles(user.Id, "system_user", true); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/mattermost/commands/roles_test.go b/cmd/mattermost/commands/roles_test.go new file mode 100644 index 000000000..0144bf0df --- /dev/null +++ b/cmd/mattermost/commands/roles_test.go @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "testing" + + "github.com/mattermost/mattermost-server/api4" + "github.com/mattermost/mattermost-server/model" +) + +func TestAssignRole(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + CheckCommand(t, "roles", "system_admin", th.BasicUser.Email) + + if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil { + t.Fatal() + } else { + user := result.Data.(*model.User) + if user.Roles != "system_admin system_user" { + t.Fatal() + } + } +} diff --git a/cmd/mattermost/commands/root.go b/cmd/mattermost/commands/root.go new file mode 100644 index 000000000..7ae439161 --- /dev/null +++ b/cmd/mattermost/commands/root.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "github.com/spf13/cobra" +) + +type Command = cobra.Command + +func Run(args []string) error { + RootCmd.SetArgs(args) + return RootCmd.Execute() +} + +var RootCmd = &cobra.Command{ + Use: "platform", + Short: "Open source, self-hosted Slack-alternative", + Long: `Mattermost offers workplace messaging across web, PC and phones with archiving, search and integration with your existing systems. Documentation available at https://docs.mattermost.com`, +} + +func init() { + RootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.") + RootCmd.PersistentFlags().Bool("disableconfigwatch", false, "When set config.json will not be loaded from disk when the file is changed.") + RootCmd.PersistentFlags().Bool("platform", false, "This flag signifies that the user tried to start the command from the platform binary, so we can log a mssage") + RootCmd.PersistentFlags().MarkHidden("platform") +} diff --git a/cmd/mattermost/commands/sampledata.go b/cmd/mattermost/commands/sampledata.go new file mode 100644 index 000000000..0051679eb --- /dev/null +++ b/cmd/mattermost/commands/sampledata.go @@ -0,0 +1,636 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "math/rand" + "os" + "path" + "sort" + "strings" + "time" + + "github.com/icrowley/fake" + "github.com/mattermost/mattermost-server/app" + "github.com/spf13/cobra" +) + +var SampleDataCmd = &cobra.Command{ + Use: "sampledata", + Short: "Generate sample data", + RunE: sampleDataCmdF, +} + +func init() { + SampleDataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).") + SampleDataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.") + SampleDataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.") + SampleDataCmd.Flags().IntP("users", "u", 15, "The number of sample users.") + SampleDataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.") + SampleDataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.") + SampleDataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.") + SampleDataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.") + SampleDataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.") + SampleDataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.") + SampleDataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.") + SampleDataCmd.Flags().IntP("workers", "w", 2, "How many workers to run during the import.") + SampleDataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.") + SampleDataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of loading into the database.") + RootCmd.AddCommand(SampleDataCmd) +} + +func sliceIncludes(vs []string, t string) bool { + for _, v := range vs { + if v == t { + return true + } + } + return false +} + +func randomPastTime(seconds int) int64 { + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.FixedZone("UTC", 0)) + return today.Unix() - int64(rand.Intn(seconds*1000)) +} + +func randomEmoji() string { + emojis := []string{"+1", "-1", "heart", "blush"} + return emojis[rand.Intn(len(emojis))] +} + +func randomReaction(users []string, parentCreateAt int64) app.ReactionImportData { + user := users[rand.Intn(len(users))] + emoji := randomEmoji() + date := parentCreateAt + int64(rand.Intn(100000)) + return app.ReactionImportData{ + User: &user, + EmojiName: &emoji, + CreateAt: &date, + } +} + +func randomReply(users []string, parentCreateAt int64) app.ReplyImportData { + user := users[rand.Intn(len(users))] + message := randomMessage(users) + date := parentCreateAt + int64(rand.Intn(100000)) + return app.ReplyImportData{ + User: &user, + Message: &message, + CreateAt: &date, + } +} + +func randomMessage(users []string) string { + var message string + switch rand.Intn(30) { + case 0: + mention := users[rand.Intn(len(users))] + message = "@" + mention + " " + fake.Sentence() + case 1: + switch rand.Intn(2) { + case 0: + mattermostVideos := []string{"Q4MgnxbpZas", "BFo7E9-Kc_E", "LsMLR-BHsKg", "MRmGDhlMhNA", "mUOPxT7VgWc"} + message = "https://www.youtube.com/watch?v=" + mattermostVideos[rand.Intn(len(mattermostVideos))] + case 1: + mattermostTweets := []string{"943119062334353408", "949370809528832005", "948539688171819009", "939122439115681792", "938061722027425797"} + message = "https://twitter.com/mattermosthq/status/" + mattermostTweets[rand.Intn(len(mattermostTweets))] + } + case 2: + message = "" + if rand.Intn(2) == 0 { + message += fake.Sentence() + } + for i := 0; i < rand.Intn(4)+1; i++ { + message += "\n * " + fake.Word() + } + default: + if rand.Intn(2) == 0 { + message = fake.Sentence() + } else { + message = fake.Paragraph() + } + if rand.Intn(3) == 0 { + message += "\n" + fake.Sentence() + } + if rand.Intn(3) == 0 { + message += "\n" + fake.Sentence() + } + if rand.Intn(3) == 0 { + message += "\n" + fake.Sentence() + } + } + return message +} + +func sampleDataCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + seed, err := command.Flags().GetInt64("seed") + if err != nil { + return errors.New("Invalid seed parameter") + } + bulk, err := command.Flags().GetString("bulk") + if err != nil { + return errors.New("Invalid bulk parameter") + } + teams, err := command.Flags().GetInt("teams") + if err != nil || teams < 0 { + return errors.New("Invalid teams parameter") + } + channelsPerTeam, err := command.Flags().GetInt("channels-per-team") + if err != nil || channelsPerTeam < 0 { + return errors.New("Invalid channels-per-team parameter") + } + users, err := command.Flags().GetInt("users") + if err != nil || users < 0 { + return errors.New("Invalid users parameter") + } + teamMemberships, err := command.Flags().GetInt("team-memberships") + if err != nil || teamMemberships < 0 { + return errors.New("Invalid team-memberships parameter") + } + channelMemberships, err := command.Flags().GetInt("channel-memberships") + if err != nil || channelMemberships < 0 { + return errors.New("Invalid channel-memberships parameter") + } + postsPerChannel, err := command.Flags().GetInt("posts-per-channel") + if err != nil || postsPerChannel < 0 { + return errors.New("Invalid posts-per-channel parameter") + } + directChannels, err := command.Flags().GetInt("direct-channels") + if err != nil || directChannels < 0 { + return errors.New("Invalid direct-channels parameter") + } + postsPerDirectChannel, err := command.Flags().GetInt("posts-per-direct-channel") + if err != nil || postsPerDirectChannel < 0 { + return errors.New("Invalid posts-per-direct-channel parameter") + } + groupChannels, err := command.Flags().GetInt("group-channels") + if err != nil || groupChannels < 0 { + return errors.New("Invalid group-channels parameter") + } + postsPerGroupChannel, err := command.Flags().GetInt("posts-per-group-channel") + if err != nil || postsPerGroupChannel < 0 { + return errors.New("Invalid posts-per-group-channel parameter") + } + workers, err := command.Flags().GetInt("workers") + if err != nil { + return errors.New("Invalid workers parameter") + } + profileImagesPath, err := command.Flags().GetString("profile-images") + if err != nil { + return errors.New("Invalid profile-images parameter") + } + profileImages := []string{} + if profileImagesPath != "" { + profileImagesStat, err := os.Stat(profileImagesPath) + if os.IsNotExist(err) { + return errors.New("Profile images folder doesn't exists.") + } + if !profileImagesStat.IsDir() { + return errors.New("profile-images parameters must be a folder path.") + } + profileImagesFiles, err := ioutil.ReadDir(profileImagesPath) + if err != nil { + return errors.New("Invalid profile-images parameter") + } + for _, profileImage := range profileImagesFiles { + profileImages = append(profileImages, path.Join(profileImagesPath, profileImage.Name())) + } + sort.Strings(profileImages) + } + + if workers < 1 { + return errors.New("You must have at least one worker.") + } + if teamMemberships > teams { + return errors.New("You can't have more team memberships than teams.") + } + if channelMemberships > channelsPerTeam { + return errors.New("You can't have more channel memberships than channels per team.") + } + + var bulkFile *os.File + switch bulk { + case "": + bulkFile, err = ioutil.TempFile("", ".mattermost-sample-data-") + defer os.Remove(bulkFile.Name()) + if err != nil { + return errors.New("Unable to open temporary file.") + } + case "-": + bulkFile = os.Stdout + default: + bulkFile, err = os.OpenFile(bulk, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return errors.New("Unable to write into the \"" + bulk + "\" file.") + } + } + + encoder := json.NewEncoder(bulkFile) + version := 1 + encoder.Encode(app.LineImportData{Type: "version", Version: &version}) + + fake.Seed(seed) + rand.Seed(seed) + + teamsAndChannels := make(map[string][]string) + for i := 0; i < teams; i++ { + teamLine := createTeam(i) + teamsAndChannels[*teamLine.Team.Name] = []string{} + encoder.Encode(teamLine) + } + + teamsList := []string{} + for teamName := range teamsAndChannels { + teamsList = append(teamsList, teamName) + } + sort.Strings(teamsList) + + for _, teamName := range teamsList { + for i := 0; i < channelsPerTeam; i++ { + channelLine := createChannel(i, teamName) + teamsAndChannels[teamName] = append(teamsAndChannels[teamName], *channelLine.Channel.Name) + encoder.Encode(channelLine) + } + } + + allUsers := []string{} + for i := 0; i < users; i++ { + userLine := createUser(i, teamMemberships, channelMemberships, teamsAndChannels, profileImages) + encoder.Encode(userLine) + allUsers = append(allUsers, *userLine.User.Username) + } + + for team, channels := range teamsAndChannels { + for _, channel := range channels { + for i := 0; i < postsPerChannel; i++ { + postLine := createPost(team, channel, allUsers) + encoder.Encode(postLine) + } + } + } + + for i := 0; i < directChannels; i++ { + user1 := allUsers[rand.Intn(len(allUsers))] + user2 := allUsers[rand.Intn(len(allUsers))] + channelLine := createDirectChannel([]string{user1, user2}) + encoder.Encode(channelLine) + for j := 0; j < postsPerDirectChannel; j++ { + postLine := createDirectPost([]string{user1, user2}) + encoder.Encode(postLine) + } + } + + for i := 0; i < groupChannels; i++ { + users := []string{} + totalUsers := 3 + rand.Intn(3) + for len(users) < totalUsers { + user := allUsers[rand.Intn(len(allUsers))] + if !sliceIncludes(users, user) { + users = append(users, user) + } + } + channelLine := createDirectChannel(users) + encoder.Encode(channelLine) + for j := 0; j < postsPerGroupChannel; j++ { + postLine := createDirectPost(users) + encoder.Encode(postLine) + } + } + + if bulk == "" { + _, err := bulkFile.Seek(0, 0) + if err != nil { + return errors.New("Unable to read correctly the temporary file.") + } + importErr, lineNumber := a.BulkImport(bulkFile, false, workers) + if importErr != nil { + return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber) + } + } else if bulk != "-" { + err := bulkFile.Close() + if err != nil { + return errors.New("Unable to close correctly the output file") + } + } + + return nil +} + +func createUser(idx int, teamMemberships int, channelMemberships int, teamsAndChannels map[string][]string, profileImages []string) app.LineImportData { + password := fmt.Sprintf("user-%d", idx) + email := fmt.Sprintf("user-%d@sample.mattermost.com", idx) + firstName := fake.FirstName() + lastName := fake.LastName() + username := fmt.Sprintf("%s.%s", strings.ToLower(firstName), strings.ToLower(lastName)) + if idx == 0 { + username = "sysadmin" + password = "sysadmin" + email = "sysadmin@sample.mattermost.com" + } else if idx == 1 { + username = "user-1" + } + position := fake.JobTitle() + roles := "system_user" + if idx%5 == 0 { + roles = "system_admin system_user" + } + + // The 75% of the users have custom profile image + var profileImage *string = nil + if rand.Intn(4) != 0 { + profileImageSelector := rand.Int() + if len(profileImages) > 0 { + profileImage = &profileImages[profileImageSelector%len(profileImages)] + } + } + + useMilitaryTime := "false" + if idx != 0 && rand.Intn(2) == 0 { + useMilitaryTime = "true" + } + + collapsePreviews := "false" + if idx != 0 && rand.Intn(2) == 0 { + collapsePreviews = "true" + } + + messageDisplay := "clean" + if idx != 0 && rand.Intn(2) == 0 { + messageDisplay = "compact" + } + + channelDisplayMode := "full" + if idx != 0 && rand.Intn(2) == 0 { + channelDisplayMode = "centered" + } + + // Some users has nickname + nickname := "" + if rand.Intn(5) == 0 { + nickname = fake.Company() + } + + // Half of users skip tutorial + tutorialStep := "999" + switch rand.Intn(6) { + case 1: + tutorialStep = "1" + case 2: + tutorialStep = "2" + case 3: + tutorialStep = "3" + } + + teams := []app.UserTeamImportData{} + possibleTeams := []string{} + for teamName := range teamsAndChannels { + possibleTeams = append(possibleTeams, teamName) + } + sort.Strings(possibleTeams) + for x := 0; x < teamMemberships; x++ { + if len(possibleTeams) == 0 { + break + } + position := rand.Intn(len(possibleTeams)) + team := possibleTeams[position] + possibleTeams = append(possibleTeams[:position], possibleTeams[position+1:]...) + if teamChannels, err := teamsAndChannels[team]; err { + teams = append(teams, createTeamMembership(channelMemberships, teamChannels, &team)) + } + } + + user := app.UserImportData{ + ProfileImage: profileImage, + Username: &username, + Email: &email, + Password: &password, + Nickname: &nickname, + FirstName: &firstName, + LastName: &lastName, + Position: &position, + Roles: &roles, + Teams: &teams, + UseMilitaryTime: &useMilitaryTime, + CollapsePreviews: &collapsePreviews, + MessageDisplay: &messageDisplay, + ChannelDisplayMode: &channelDisplayMode, + TutorialStep: &tutorialStep, + } + return app.LineImportData{ + Type: "user", + User: &user, + } +} + +func createTeamMembership(numOfchannels int, teamChannels []string, teamName *string) app.UserTeamImportData { + roles := "team_user" + if rand.Intn(5) == 0 { + roles = "team_user team_admin" + } + channels := []app.UserChannelImportData{} + teamChannelsCopy := append([]string(nil), teamChannels...) + for x := 0; x < numOfchannels; x++ { + if len(teamChannelsCopy) == 0 { + break + } + position := rand.Intn(len(teamChannelsCopy)) + channelName := teamChannelsCopy[position] + teamChannelsCopy = append(teamChannelsCopy[:position], teamChannelsCopy[position+1:]...) + channels = append(channels, createChannelMembership(channelName)) + } + + return app.UserTeamImportData{ + Name: teamName, + Roles: &roles, + Channels: &channels, + } +} + +func createChannelMembership(channelName string) app.UserChannelImportData { + roles := "channel_user" + if rand.Intn(5) == 0 { + roles = "channel_user channel_admin" + } + favorite := rand.Intn(5) == 0 + + return app.UserChannelImportData{ + Name: &channelName, + Roles: &roles, + Favorite: &favorite, + } +} + +func createTeam(idx int) app.LineImportData { + displayName := fake.Word() + name := fmt.Sprintf("%s-%d", fake.Word(), idx) + allowOpenInvite := rand.Intn(2) == 0 + + description := fake.Paragraph() + if len(description) > 255 { + description = description[0:255] + } + + teamType := "O" + if rand.Intn(2) == 0 { + teamType = "I" + } + + team := app.TeamImportData{ + DisplayName: &displayName, + Name: &name, + AllowOpenInvite: &allowOpenInvite, + Description: &description, + Type: &teamType, + } + return app.LineImportData{ + Type: "team", + Team: &team, + } +} + +func createChannel(idx int, teamName string) app.LineImportData { + displayName := fake.Word() + name := fmt.Sprintf("%s-%d", fake.Word(), idx) + header := fake.Paragraph() + purpose := fake.Paragraph() + + if len(purpose) > 250 { + purpose = purpose[0:250] + } + + channelType := "P" + if rand.Intn(2) == 0 { + channelType = "O" + } + + channel := app.ChannelImportData{ + Team: &teamName, + Name: &name, + DisplayName: &displayName, + Type: &channelType, + Header: &header, + Purpose: &purpose, + } + return app.LineImportData{ + Type: "channel", + Channel: &channel, + } +} + +func createPost(team string, channel string, allUsers []string) app.LineImportData { + message := randomMessage(allUsers) + create_at := randomPastTime(50000) + user := allUsers[rand.Intn(len(allUsers))] + + // Some messages are flagged by an user + flagged_by := []string{} + if rand.Intn(10) == 0 { + flagged_by = append(flagged_by, allUsers[rand.Intn(len(allUsers))]) + } + + reactions := []app.ReactionImportData{} + if rand.Intn(10) == 0 { + for { + reactions = append(reactions, randomReaction(allUsers, create_at)) + if rand.Intn(3) == 0 { + break + } + } + } + + replies := []app.ReplyImportData{} + if rand.Intn(10) == 0 { + for { + replies = append(replies, randomReply(allUsers, create_at)) + if rand.Intn(4) == 0 { + break + } + } + } + + post := app.PostImportData{ + Team: &team, + Channel: &channel, + User: &user, + Message: &message, + CreateAt: &create_at, + FlaggedBy: &flagged_by, + Reactions: &reactions, + Replies: &replies, + } + return app.LineImportData{ + Type: "post", + Post: &post, + } +} + +func createDirectChannel(members []string) app.LineImportData { + header := fake.Sentence() + + channel := app.DirectChannelImportData{ + Members: &members, + Header: &header, + } + return app.LineImportData{ + Type: "direct_channel", + DirectChannel: &channel, + } +} + +func createDirectPost(members []string) app.LineImportData { + message := randomMessage(members) + create_at := randomPastTime(50000) + user := members[rand.Intn(len(members))] + + // Some messages are flagged by an user + flagged_by := []string{} + if rand.Intn(10) == 0 { + flagged_by = append(flagged_by, members[rand.Intn(len(members))]) + } + + reactions := []app.ReactionImportData{} + if rand.Intn(10) == 0 { + for { + reactions = append(reactions, randomReaction(members, create_at)) + if rand.Intn(3) == 0 { + break + } + } + } + + replies := []app.ReplyImportData{} + if rand.Intn(10) == 0 { + for { + replies = append(replies, randomReply(members, create_at)) + if rand.Intn(4) == 0 { + break + } + } + } + + post := app.DirectPostImportData{ + ChannelMembers: &members, + User: &user, + Message: &message, + CreateAt: &create_at, + FlaggedBy: &flagged_by, + Reactions: &reactions, + Replies: &replies, + } + return app.LineImportData{ + Type: "direct_post", + DirectPost: &post, + } +} diff --git a/cmd/mattermost/commands/sampledata_test.go b/cmd/mattermost/commands/sampledata_test.go new file mode 100644 index 000000000..e447fe492 --- /dev/null +++ b/cmd/mattermost/commands/sampledata_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "testing" + + "github.com/mattermost/mattermost-server/api4" + "github.com/stretchr/testify/require" +) + +func TestSampledataBadParameters(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + // should fail because you need at least 1 worker + require.Error(t, RunCommand(t, "sampledata", "--workers", "0")) + + // should fail because you have more team memberships than teams + require.Error(t, RunCommand(t, "sampledata", "--teams", "10", "--teams-memberships", "11")) + + // should fail because you have more channel memberships than channels per team + require.Error(t, RunCommand(t, "sampledata", "--channels-per-team", "10", "--channel-memberships", "11")) +} diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go new file mode 100644 index 000000000..9d0e5a917 --- /dev/null +++ b/cmd/mattermost/commands/server.go @@ -0,0 +1,299 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "fmt" + "net" + "os" + "os/signal" + "syscall" + "time" + + "github.com/mattermost/mattermost-server/api4" + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/manualtesting" + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/mattermost/mattermost-server/web" + "github.com/mattermost/mattermost-server/wsapi" + "github.com/spf13/cobra" +) + +const ( + SESSIONS_CLEANUP_BATCH_SIZE = 1000 +) + +var MaxNotificationsPerChannelDefault int64 = 1000000 + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Run the Mattermost server", + RunE: serverCmdF, + SilenceUsage: true, +} + +func init() { + RootCmd.AddCommand(serverCmd) + RootCmd.RunE = serverCmdF +} + +func serverCmdF(command *cobra.Command, args []string) error { + config, err := command.Flags().GetString("config") + if err != nil { + return err + } + + disableConfigWatch, _ := command.Flags().GetBool("disableconfigwatch") + usedPlatform, _ := command.Flags().GetBool("platform") + + interruptChan := make(chan os.Signal, 1) + return runServer(config, disableConfigWatch, usedPlatform, interruptChan) +} + +func runServer(configFileLocation string, disableConfigWatch bool, usedPlatform bool, interruptChan chan os.Signal) error { + options := []app.Option{app.ConfigFile(configFileLocation)} + if disableConfigWatch { + options = append(options, app.DisableConfigWatch) + } + + a, err := app.New(options...) + if err != nil { + mlog.Critical(err.Error()) + return err + } + defer a.Shutdown() + + utils.TestConnection(a.Config()) + + pwd, _ := os.Getwd() + if usedPlatform { + mlog.Error("The platform binary has been deprecated, please switch to using the mattermost binary.") + } + mlog.Info(fmt.Sprintf("Current version is %v (%v/%v/%v/%v)", model.CurrentVersion, model.BuildNumber, model.BuildDate, model.BuildHash, model.BuildHashEnterprise)) + mlog.Info(fmt.Sprintf("Enterprise Enabled: %v", model.BuildEnterpriseReady)) + mlog.Info(fmt.Sprintf("Current working directory is %v", pwd)) + mlog.Info(fmt.Sprintf("Loaded config file from %v", utils.FindConfigFile(configFileLocation))) + + backend, appErr := a.FileBackend() + if appErr == nil { + appErr = backend.TestConnection() + } + if appErr != nil { + mlog.Error("Problem with file storage settings: " + appErr.Error()) + } + + if model.BuildEnterpriseReady == "true" { + a.LoadLicense() + } + + a.DoAdvancedPermissionsMigration() + + a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil) + a.AddConfigListener(func(prevCfg, cfg *model.Config) { + if *cfg.PluginSettings.Enable { + a.InitPlugins(*cfg.PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil) + } else { + a.ShutDownPlugins() + } + }) + + serverErr := a.StartServer() + if serverErr != nil { + mlog.Critical(serverErr.Error()) + return serverErr + } + + api := api4.Init(a, a.Srv.Router) + wsapi.Init(a, a.Srv.WebSocketRouter) + web.NewWeb(a, a.Srv.Router) + + license := a.License() + + if license == nil && len(a.Config().SqlSettings.DataSourceReplicas) > 1 { + mlog.Warn("More than 1 read replica functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license.") + a.UpdateConfig(func(cfg *model.Config) { + cfg.SqlSettings.DataSourceReplicas = cfg.SqlSettings.DataSourceReplicas[:1] + }) + } + + if license == nil { + a.UpdateConfig(func(cfg *model.Config) { + cfg.TeamSettings.MaxNotificationsPerChannel = &MaxNotificationsPerChannelDefault + }) + } + + a.ReloadConfig() + + // Enable developer settings if this is a "dev" build + if model.BuildNumber == "dev" { + a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true }) + } + + resetStatuses(a) + + // If we allow testing then listen for manual testing URL hits + if a.Config().ServiceSettings.EnableTesting { + manualtesting.Init(api) + } + + a.EnsureDiagnosticId() + + a.Go(func() { + runSecurityJob(a) + }) + a.Go(func() { + runDiagnosticsJob(a) + }) + a.Go(func() { + runSessionCleanupJob(a) + }) + a.Go(func() { + runTokenCleanupJob(a) + }) + a.Go(func() { + runCommandWebhookCleanupJob(a) + }) + + if complianceI := a.Compliance; complianceI != nil { + complianceI.StartComplianceDailyJob() + } + + if a.Cluster != nil { + a.RegisterAllClusterMessageHandlers() + a.Cluster.StartInterNodeCommunication() + } + + if a.Metrics != nil { + a.Metrics.StartServer() + } + + if a.Elasticsearch != nil { + a.Go(func() { + if err := a.Elasticsearch.Start(); err != nil { + mlog.Error(err.Error()) + } + }) + } + + if *a.Config().JobSettings.RunJobs { + a.Jobs.StartWorkers() + } + if *a.Config().JobSettings.RunScheduler { + a.Jobs.StartSchedulers() + } + + notifyReady() + + // wait for kill signal before attempting to gracefully shutdown + // the running service + signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-interruptChan + + if a.Cluster != nil { + a.Cluster.StopInterNodeCommunication() + } + + if a.Metrics != nil { + a.Metrics.StopServer() + } + + a.Jobs.StopSchedulers() + a.Jobs.StopWorkers() + + return nil +} + +func runSecurityJob(a *app.App) { + doSecurity(a) + model.CreateRecurringTask("Security", func() { + doSecurity(a) + }, time.Hour*4) +} + +func runDiagnosticsJob(a *app.App) { + doDiagnostics(a) + model.CreateRecurringTask("Diagnostics", func() { + doDiagnostics(a) + }, time.Hour*24) +} + +func runTokenCleanupJob(a *app.App) { + doTokenCleanup(a) + model.CreateRecurringTask("Token Cleanup", func() { + doTokenCleanup(a) + }, time.Hour*1) +} + +func runCommandWebhookCleanupJob(a *app.App) { + doCommandWebhookCleanup(a) + model.CreateRecurringTask("Command Hook Cleanup", func() { + doCommandWebhookCleanup(a) + }, time.Hour*1) +} + +func runSessionCleanupJob(a *app.App) { + doSessionCleanup(a) + model.CreateRecurringTask("Session Cleanup", func() { + doSessionCleanup(a) + }, time.Hour*24) +} + +func resetStatuses(a *app.App) { + if result := <-a.Srv.Store.Status().ResetAll(); result.Err != nil { + mlog.Error(fmt.Sprint("mattermost.reset_status.error FIXME: NOT FOUND IN TRANSLATIONS FILE", result.Err.Error())) + } +} + +func doSecurity(a *app.App) { + a.DoSecurityUpdateCheck() +} + +func doDiagnostics(a *app.App) { + if *a.Config().LogSettings.EnableDiagnostics { + a.SendDailyDiagnostics() + } +} + +func notifyReady() { + // If the environment vars provide a systemd notification socket, + // notify systemd that the server is ready. + systemdSocket := os.Getenv("NOTIFY_SOCKET") + if systemdSocket != "" { + mlog.Info("Sending systemd READY notification.") + + err := sendSystemdReadyNotification(systemdSocket) + if err != nil { + mlog.Error(err.Error()) + } + } +} + +func sendSystemdReadyNotification(socketPath string) error { + msg := "READY=1" + addr := &net.UnixAddr{ + Name: socketPath, + Net: "unixgram", + } + conn, err := net.DialUnix(addr.Net, nil, addr) + if err != nil { + return err + } + defer conn.Close() + _, err = conn.Write([]byte(msg)) + return err +} + +func doTokenCleanup(a *app.App) { + a.Srv.Store.Token().Cleanup() +} + +func doCommandWebhookCleanup(a *app.App) { + a.Srv.Store.CommandWebhook().Cleanup() +} + +func doSessionCleanup(a *app.App) { + a.Srv.Store.Session().Cleanup(model.GetMillis(), SESSIONS_CLEANUP_BATCH_SIZE) +} diff --git a/cmd/mattermost/commands/server_test.go b/cmd/mattermost/commands/server_test.go new file mode 100644 index 000000000..0f825e316 --- /dev/null +++ b/cmd/mattermost/commands/server_test.go @@ -0,0 +1,136 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "io/ioutil" + "net" + "os" + "syscall" + "testing" + + "github.com/mattermost/mattermost-server/jobs" + "github.com/mattermost/mattermost-server/utils" + "github.com/stretchr/testify/require" +) + +type ServerTestHelper struct { + configPath string + disableConfigWatch bool + interruptChan chan os.Signal + originalInterval int +} + +func SetupServerTest() *ServerTestHelper { + // Build a channel that will be used by the server to receive system signals… + interruptChan := make(chan os.Signal, 1) + // …and sent it immediately a SIGINT value. + // This will make the server loop stop as soon as it started successfully. + interruptChan <- syscall.SIGINT + + // Let jobs poll for termination every 0.2s (instead of every 15s by default) + // Otherwise we would have to wait the whole polling duration before the test + // terminates. + originalInterval := jobs.DEFAULT_WATCHER_POLLING_INTERVAL + jobs.DEFAULT_WATCHER_POLLING_INTERVAL = 200 + + th := &ServerTestHelper{ + configPath: utils.FindConfigFile("config.json"), + disableConfigWatch: true, + interruptChan: interruptChan, + originalInterval: originalInterval, + } + return th +} + +func (th *ServerTestHelper) TearDownServerTest() { + jobs.DEFAULT_WATCHER_POLLING_INTERVAL = th.originalInterval +} + +func TestRunServerSuccess(t *testing.T) { + th := SetupServerTest() + defer th.TearDownServerTest() + + err := runServer(th.configPath, th.disableConfigWatch, false, th.interruptChan) + require.NoError(t, err) +} + +func TestRunServerInvalidConfigFile(t *testing.T) { + th := SetupServerTest() + defer th.TearDownServerTest() + + // Start the server with an unreadable config file + unreadableConfigFile, err := ioutil.TempFile("", "mattermost-unreadable-config-file-") + if err != nil { + panic(err) + } + os.Chmod(unreadableConfigFile.Name(), 0200) + defer os.Remove(unreadableConfigFile.Name()) + + err = runServer(unreadableConfigFile.Name(), th.disableConfigWatch, false, th.interruptChan) + require.Error(t, err) +} + +func TestRunServerSystemdNotification(t *testing.T) { + th := SetupServerTest() + defer th.TearDownServerTest() + + // Get a random temporary filename for using as a mock systemd socket + socketFile, err := ioutil.TempFile("", "mattermost-systemd-mock-socket-") + if err != nil { + panic(err) + } + socketPath := socketFile.Name() + os.Remove(socketPath) + + // Set the socket path in the process environment + originalSocket := os.Getenv("NOTIFY_SOCKET") + os.Setenv("NOTIFY_SOCKET", socketPath) + defer os.Setenv("NOTIFY_SOCKET", originalSocket) + + // Open the socket connection + addr := &net.UnixAddr{ + Name: socketPath, + Net: "unixgram", + } + connection, err := net.ListenUnixgram("unixgram", addr) + if err != nil { + panic(err) + } + defer connection.Close() + defer os.Remove(socketPath) + + // Listen for socket data + socketReader := make(chan string) + go func(ch chan string) { + buffer := make([]byte, 512) + count, err := connection.Read(buffer) + if err != nil { + panic(err) + } + data := buffer[0:count] + ch <- string(data) + }(socketReader) + + // Start and stop the server + err = runServer(th.configPath, th.disableConfigWatch, false, th.interruptChan) + require.NoError(t, err) + + // Ensure the notification has been sent on the socket and is correct + notification := <-socketReader + require.Equal(t, notification, "READY=1") +} + +func TestRunServerNoSystemd(t *testing.T) { + th := SetupServerTest() + defer th.TearDownServerTest() + + // Temporarily remove any Systemd socket defined in the environment + originalSocket := os.Getenv("NOTIFY_SOCKET") + os.Unsetenv("NOTIFY_SOCKET") + defer os.Setenv("NOTIFY_SOCKET", originalSocket) + + err := runServer(th.configPath, th.disableConfigWatch, false, th.interruptChan) + require.NoError(t, err) +} diff --git a/cmd/mattermost/commands/team.go b/cmd/mattermost/commands/team.go new file mode 100644 index 000000000..59570d5e7 --- /dev/null +++ b/cmd/mattermost/commands/team.go @@ -0,0 +1,249 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "errors" + "fmt" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" + "github.com/spf13/cobra" +) + +var TeamCmd = &cobra.Command{ + Use: "team", + Short: "Management of teams", +} + +var TeamCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a team", + Long: `Create a team.`, + Example: ` team create --name mynewteam --display_name "My New Team" + team create --name private --display_name "My New Private Team" --private`, + RunE: createTeamCmdF, +} + +var RemoveUsersCmd = &cobra.Command{ + Use: "remove [team] [users]", + Short: "Remove users from team", + Long: "Remove some users from team", + Example: " team remove myteam user@example.com username", + RunE: removeUsersCmdF, +} + +var AddUsersCmd = &cobra.Command{ + Use: "add [team] [users]", + Short: "Add users to team", + Long: "Add some users to team", + Example: " team add myteam user@example.com username", + RunE: addUsersCmdF, +} + +var DeleteTeamsCmd = &cobra.Command{ + Use: "delete [teams]", + Short: "Delete teams", + Long: `Permanently delete some teams. +Permanently deletes a team along with all related information including posts from the database.`, + Example: " team delete myteam", + RunE: deleteTeamsCmdF, +} + +var ListTeamsCmd = &cobra.Command{ + Use: "list", + Short: "List all teams.", + Long: `List all teams on the server.`, + Example: " team list", + RunE: listTeamsCmdF, +} + +func init() { + TeamCreateCmd.Flags().String("name", "", "Team Name") + TeamCreateCmd.Flags().String("display_name", "", "Team Display Name") + TeamCreateCmd.Flags().Bool("private", false, "Create a private team.") + TeamCreateCmd.Flags().String("email", "", "Administrator Email (anyone with this email is automatically a team admin)") + + DeleteTeamsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the team and a DB backup has been performed.") + + TeamCmd.AddCommand( + TeamCreateCmd, + RemoveUsersCmd, + AddUsersCmd, + DeleteTeamsCmd, + ListTeamsCmd, + ) + RootCmd.AddCommand(TeamCmd) +} + +func createTeamCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + name, errn := command.Flags().GetString("name") + if errn != nil || name == "" { + return errors.New("Name is required") + } + displayname, errdn := command.Flags().GetString("display_name") + if errdn != nil || displayname == "" { + return errors.New("Display Name is required") + } + email, _ := command.Flags().GetString("email") + useprivate, _ := command.Flags().GetBool("private") + + teamType := model.TEAM_OPEN + if useprivate { + teamType = model.TEAM_INVITE + } + + team := &model.Team{ + Name: name, + DisplayName: displayname, + Email: email, + Type: teamType, + } + + if _, err := a.CreateTeam(team); err != nil { + return errors.New("Team creation failed: " + err.Error()) + } + + return nil +} + +func removeUsersCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + team := getTeamFromTeamArg(a, args[0]) + if team == nil { + return errors.New("Unable to find team '" + args[0] + "'") + } + + users := getUsersFromUserArgs(a, args[1:]) + for i, user := range users { + removeUserFromTeam(a, team, user, args[i+1]) + } + + return nil +} + +func removeUserFromTeam(a *app.App, team *model.Team, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if err := a.LeaveTeam(team, user, ""); err != nil { + CommandPrintErrorln("Unable to remove '" + userArg + "' from " + team.Name + ". Error: " + err.Error()) + } +} + +func addUsersCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + team := getTeamFromTeamArg(a, args[0]) + if team == nil { + return errors.New("Unable to find team '" + args[0] + "'") + } + + users := getUsersFromUserArgs(a, args[1:]) + for i, user := range users { + addUserToTeam(a, team, user, args[i+1]) + } + + return nil +} + +func addUserToTeam(a *app.App, team *model.Team, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if err := a.JoinUserToTeam(team, user, ""); err != nil { + CommandPrintErrorln("Unable to add '" + userArg + "' to " + team.Name) + } +} + +func deleteTeamsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Not enough arguments.") + } + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + CommandPrettyPrintln("Are you sure you want to delete the teams specified? All data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + teams := getTeamsFromTeamArgs(a, args) + for i, team := range teams { + if team == nil { + CommandPrintErrorln("Unable to find team '" + args[i] + "'") + continue + } + if err := deleteTeam(a, team); err != nil { + CommandPrintErrorln("Unable to delete team '" + team.Name + "' error: " + err.Error()) + } else { + CommandPrettyPrintln("Deleted team '" + team.Name + "'") + } + } + + return nil +} + +func deleteTeam(a *app.App, team *model.Team) *model.AppError { + return a.PermanentDeleteTeam(team) +} + +func listTeamsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + teams, err2 := a.GetAllTeams() + if err2 != nil { + return err2 + } + + for _, team := range teams { + CommandPrettyPrintln(team.Name) + } + + return nil +} diff --git a/cmd/mattermost/commands/team_test.go b/cmd/mattermost/commands/team_test.go new file mode 100644 index 000000000..20e04bdc9 --- /dev/null +++ b/cmd/mattermost/commands/team_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "strings" + "testing" + + "github.com/mattermost/mattermost-server/api4" + "github.com/mattermost/mattermost-server/model" +) + +func TestCreateTeam(t *testing.T) { + th := api4.Setup().InitSystemAdmin() + defer th.TearDown() + + id := model.NewId() + name := "name" + id + displayName := "Name " + id + + CheckCommand(t, "team", "create", "--name", name, "--display_name", displayName) + + found := th.SystemAdminClient.Must(th.SystemAdminClient.TeamExists(name, "")).(bool) + + if !found { + t.Fatal("Failed to create Team") + } +} + +func TestJoinTeam(t *testing.T) { + th := api4.Setup().InitSystemAdmin().InitBasic() + defer th.TearDown() + + CheckCommand(t, "team", "add", th.BasicTeam.Name, th.BasicUser.Email) + + profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetUsersInTeam(th.BasicTeam.Id, 0, 1000, "")).([]*model.User) + + found := false + + for _, user := range profiles { + if user.Email == th.BasicUser.Email { + found = true + } + + } + + if !found { + t.Fatal("Failed to create User") + } +} + +func TestLeaveTeam(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + CheckCommand(t, "team", "remove", th.BasicTeam.Name, th.BasicUser.Email) + + profiles := th.Client.Must(th.Client.GetUsersInTeam(th.BasicTeam.Id, 0, 1000, "")).([]*model.User) + + found := false + + for _, user := range profiles { + if user.Email == th.BasicUser.Email { + found = true + } + + } + + if found { + t.Fatal("profile should not be on team") + } + + if result := <-th.App.Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil { + teamMembers := result.Data.([]*model.TeamMember) + if len(teamMembers) > 0 { + t.Fatal("Shouldn't be in team") + } + } +} + +func TestListTeams(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + id := model.NewId() + name := "name" + id + displayName := "Name " + id + + CheckCommand(t, "team", "create", "--name", name, "--display_name", displayName) + + output := CheckCommand(t, "team", "list", th.BasicTeam.Name, th.BasicUser.Email) + + if !strings.Contains(string(output), name) { + t.Fatal("should have the created team") + } +} diff --git a/cmd/mattermost/commands/teamargs.go b/cmd/mattermost/commands/teamargs.go new file mode 100644 index 000000000..aa62d52b8 --- /dev/null +++ b/cmd/mattermost/commands/teamargs.go @@ -0,0 +1,33 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" +) + +func getTeamsFromTeamArgs(a *app.App, teamArgs []string) []*model.Team { + teams := make([]*model.Team, 0, len(teamArgs)) + for _, teamArg := range teamArgs { + team := getTeamFromTeamArg(a, teamArg) + teams = append(teams, team) + } + return teams +} + +func getTeamFromTeamArg(a *app.App, teamArg string) *model.Team { + var team *model.Team + if result := <-a.Srv.Store.Team().GetByName(teamArg); result.Err == nil { + team = result.Data.(*model.Team) + } + + if team == nil { + if result := <-a.Srv.Store.Team().Get(teamArg); result.Err == nil { + team = result.Data.(*model.Team) + } + } + + return team +} diff --git a/cmd/mattermost/commands/test.go b/cmd/mattermost/commands/test.go new file mode 100644 index 000000000..b5ab37af8 --- /dev/null +++ b/cmd/mattermost/commands/test.go @@ -0,0 +1,147 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "bufio" + "fmt" + "os" + "os/exec" + + "os/signal" + "syscall" + + "github.com/mattermost/mattermost-server/api4" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/mattermost/mattermost-server/wsapi" + "github.com/spf13/cobra" +) + +var TestCmd = &cobra.Command{ + Use: "test", + Short: "Testing Commands", + Hidden: true, +} + +var RunWebClientTestsCmd = &cobra.Command{ + Use: "web_client_tests", + Short: "Run the web client tests", + RunE: webClientTestsCmdF, +} + +var RunServerForWebClientTestsCmd = &cobra.Command{ + Use: "web_client_tests_server", + Short: "Run the server configured for running the web client tests against it", + RunE: serverForWebClientTestsCmdF, +} + +func init() { + TestCmd.AddCommand( + RunWebClientTestsCmd, + RunServerForWebClientTestsCmd, + ) + RootCmd.AddCommand(TestCmd) +} + +func webClientTestsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + utils.InitTranslations(a.Config().LocalizationSettings) + serverErr := a.StartServer() + if serverErr != nil { + return serverErr + } + + api4.Init(a, a.Srv.Router) + wsapi.Init(a, a.Srv.WebSocketRouter) + a.UpdateConfig(setupClientTests) + runWebClientTests() + + return nil +} + +func serverForWebClientTestsCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + utils.InitTranslations(a.Config().LocalizationSettings) + serverErr := a.StartServer() + if serverErr != nil { + return serverErr + } + + api4.Init(a, a.Srv.Router) + wsapi.Init(a, a.Srv.WebSocketRouter) + a.UpdateConfig(setupClientTests) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-c + + return nil +} + +func setupClientTests(cfg *model.Config) { + *cfg.TeamSettings.EnableOpenServer = true + *cfg.ServiceSettings.EnableCommands = false + *cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + *cfg.ServiceSettings.EnableCustomEmoji = true + cfg.ServiceSettings.EnableIncomingWebhooks = false + cfg.ServiceSettings.EnableOutgoingWebhooks = false +} + +func executeTestCommand(command *exec.Cmd) { + cmdOutPipe, err := command.StdoutPipe() + if err != nil { + CommandPrintErrorln("Failed to run tests") + os.Exit(1) + return + } + + cmdErrOutPipe, err := command.StderrPipe() + if err != nil { + CommandPrintErrorln("Failed to run tests") + os.Exit(1) + return + } + + cmdOutReader := bufio.NewScanner(cmdOutPipe) + cmdErrOutReader := bufio.NewScanner(cmdErrOutPipe) + go func() { + for cmdOutReader.Scan() { + fmt.Println(cmdOutReader.Text()) + } + }() + + go func() { + for cmdErrOutReader.Scan() { + fmt.Println(cmdErrOutReader.Text()) + } + }() + + if err := command.Run(); err != nil { + CommandPrintErrorln("Client Tests failed") + os.Exit(1) + return + } +} + +func runWebClientTests() { + if webappDir := os.Getenv("WEBAPP_DIR"); webappDir != "" { + os.Chdir(webappDir) + } else { + os.Chdir("../mattermost-webapp") + } + + cmd := exec.Command("npm", "test") + executeTestCommand(cmd) +} diff --git a/cmd/mattermost/commands/user.go b/cmd/mattermost/commands/user.go new file mode 100644 index 000000000..ebcebcce8 --- /dev/null +++ b/cmd/mattermost/commands/user.go @@ -0,0 +1,715 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" + "github.com/spf13/cobra" +) + +var UserCmd = &cobra.Command{ + Use: "user", + Short: "Management of users", +} + +var UserActivateCmd = &cobra.Command{ + Use: "activate [emails, usernames, userIds]", + Short: "Activate users", + Long: "Activate users that have been deactivated.", + Example: ` user activate user@example.com + user activate username`, + RunE: userActivateCmdF, +} + +var UserDeactivateCmd = &cobra.Command{ + Use: "deactivate [emails, usernames, userIds]", + Short: "Deactivate users", + Long: "Deactivate users. Deactivated users are immediately logged out of all sessions and are unable to log back in.", + Example: ` user deactivate user@example.com + user deactivate username`, + RunE: userDeactivateCmdF, +} + +var UserCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a user", + Long: "Create a user", + Example: ` user create --email user@example.com --username userexample --password Password1`, + RunE: userCreateCmdF, +} + +var UserInviteCmd = &cobra.Command{ + Use: "invite [email] [teams]", + Short: "Send user an email invite to a team.", + Long: `Send user an email invite to a team. +You can invite a user to multiple teams by listing them. +You can specify teams by name or ID.`, + Example: ` user invite user@example.com myteam + user invite user@example.com myteam1 myteam2`, + RunE: userInviteCmdF, +} + +var ResetUserPasswordCmd = &cobra.Command{ + Use: "password [user] [password]", + Short: "Set a user's password", + Long: "Set a user's password", + Example: " user password user@example.com Password1", + RunE: resetUserPasswordCmdF, +} + +var updateUserEmailCmd = &cobra.Command{ + Use: "email [user] [new email]", + Short: "Change email of the user", + Long: "Change email of the user.", + Example: ` user email test user@example.com + user activate username`, + RunE: updateUserEmailCmdF, +} + +var ResetUserMfaCmd = &cobra.Command{ + Use: "resetmfa [users]", + Short: "Turn off MFA", + Long: `Turn off multi-factor authentication for a user. +If MFA enforcement is enabled, the user will be forced to re-enable MFA as soon as they login.`, + Example: " user resetmfa user@example.com", + RunE: resetUserMfaCmdF, +} + +var DeleteUserCmd = &cobra.Command{ + Use: "delete [users]", + Short: "Delete users and all posts", + Long: "Permanently delete user and all related information including posts.", + Example: " user delete user@example.com", + RunE: deleteUserCmdF, +} + +var DeleteAllUsersCmd = &cobra.Command{ + Use: "deleteall", + Short: "Delete all users and all posts", + Long: "Permanently delete all users and all related information including posts.", + Example: " user deleteall", + RunE: deleteAllUsersCommandF, +} + +var MigrateAuthCmd = &cobra.Command{ + Use: "migrate_auth [from_auth] [to_auth] [migration-options]", + Short: "Mass migrate user accounts authentication type", + Long: `Migrates accounts from one authentication provider to another. For example, you can upgrade your authentication provider from email to ldap.`, + Example: " user migrate_auth email saml users.json", + Args: func(command *cobra.Command, args []string) error { + if len(args) < 2 { + return errors.New("Auth migration requires at least 2 arguments.") + } + + toAuth := args[1] + + if toAuth != "ldap" && toAuth != "saml" { + return errors.New("Invalid to_auth parameter, must be saml or ldap.") + } + + if toAuth == "ldap" && len(args) != 3 { + return errors.New("Ldap migration requires 3 arguments.") + } + + autoFlag, _ := command.Flags().GetBool("auto") + + if toAuth == "saml" && autoFlag { + if len(args) != 2 { + return errors.New("Saml migration requires two arguments when using the --auto flag. See help text for details.") + } + } + + if toAuth == "saml" && !autoFlag { + if len(args) != 3 { + return errors.New("Saml migration requires three arguments when not using the --auto flag. See help text for details.") + } + } + return nil + }, + RunE: migrateAuthCmdF, +} + +var VerifyUserCmd = &cobra.Command{ + Use: "verify [users]", + Short: "Verify email of users", + Long: "Verify the emails of some users.", + Example: " user verify user1", + RunE: verifyUserCmdF, +} + +var SearchUserCmd = &cobra.Command{ + Use: "search [users]", + Short: "Search for users", + Long: "Search for users based on username, email, or user ID.", + Example: " user search user1@mail.com user2@mail.com", + RunE: searchUserCmdF, +} + +func init() { + UserCreateCmd.Flags().String("username", "", "Required. Username for the new user account.") + UserCreateCmd.Flags().String("email", "", "Required. The email address for the new user account.") + UserCreateCmd.Flags().String("password", "", "Required. The password for the new user account.") + UserCreateCmd.Flags().String("nickname", "", "Optional. The nickname for the new user account.") + UserCreateCmd.Flags().String("firstname", "", "Optional. The first name for the new user account.") + UserCreateCmd.Flags().String("lastname", "", "Optional. The last name for the new user account.") + UserCreateCmd.Flags().String("locale", "", "Optional. The locale (ex: en, fr) for the new user account.") + UserCreateCmd.Flags().Bool("system_admin", false, "Optional. If supplied, the new user will be a system administrator. Defaults to false.") + + DeleteUserCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") + + DeleteAllUsersCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the user and a DB backup has been performed.") + + MigrateAuthCmd.Flags().Bool("force", false, "Force the migration to occur even if there are duplicates on the LDAP server. Duplicates will not be migrated. (ldap only)") + MigrateAuthCmd.Flags().Bool("auto", false, "Automatically migrate all users. Assumes the usernames and emails are identical between Mattermost and SAML services. (saml only)") + MigrateAuthCmd.Flags().Bool("dryRun", false, "Run a simulation of the migration process without changing the database.") + MigrateAuthCmd.SetUsageTemplate(`Usage: + platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags] + +Examples: +{{.Example}} + +Arguments: + from_auth: + The authentication service to migrate users accounts from. + Supported options: email, gitlab, ldap, saml. + + to_auth: + The authentication service to migrate users to. + Supported options: ldap, saml. + + migration-options: + Migration specific options, full command help for more information. + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} +`) + MigrateAuthCmd.SetHelpTemplate(`Usage: + platform user migrate_auth [from_auth] [to_auth] [migration-options] [flags] + +Examples: +{{.Example}} + +Arguments: + from_auth: + The authentication service to migrate users accounts from. + Supported options: email, gitlab, ldap, saml. + + to_auth: + The authentication service to migrate users to. + Supported options: ldap, saml. + + migration-options (ldap): + match_field: + The field that is guaranteed to be the same in both authentication services. For example, if the users emails are consistent set to email. + Supported options: email, username. + + migration-options (saml): + users_file: + The path of a json file with the usernames and emails of all users to migrate to SAML. The username and email must be the same that the SAML service provider store. And the email must match with the email in mattermost database. + + Example json content: + { + "usr1@email.com": "usr.one", + "usr2@email.com": "usr.two" + } + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}} +`) + + UserCmd.AddCommand( + UserActivateCmd, + UserDeactivateCmd, + UserCreateCmd, + UserInviteCmd, + ResetUserPasswordCmd, + updateUserEmailCmd, + ResetUserMfaCmd, + DeleteUserCmd, + DeleteAllUsersCmd, + MigrateAuthCmd, + VerifyUserCmd, + SearchUserCmd, + ) + RootCmd.AddCommand(UserCmd) +} + +func userActivateCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Expected at least one argument. See help text for details.") + } + + changeUsersActiveStatus(a, args, true) + + return nil +} + +func changeUsersActiveStatus(a *app.App, userArgs []string, active bool) { + users := getUsersFromUserArgs(a, userArgs) + for i, user := range users { + err := changeUserActiveStatus(a, user, userArgs[i], active) + + if err != nil { + CommandPrintErrorln(err.Error()) + } + } +} + +func changeUserActiveStatus(a *app.App, user *model.User, userArg string, activate bool) error { + if user == nil { + return fmt.Errorf("Can't find user '%v'", userArg) + } + if user.IsSSOUser() { + fmt.Println("You must also deactivate this user in the SSO provider or they will be reactivated on next login or sync.") + } + if _, err := a.UpdateActive(user, activate); err != nil { + return fmt.Errorf("Unable to change activation status of user: %v", userArg) + } + + return nil +} + +func userDeactivateCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Expected at least one argument. See help text for details.") + } + + changeUsersActiveStatus(a, args, false) + + return nil +} + +func userCreateCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + username, erru := command.Flags().GetString("username") + if erru != nil || username == "" { + return errors.New("Username is required") + } + email, erre := command.Flags().GetString("email") + if erre != nil || email == "" { + return errors.New("Email is required") + } + password, errp := command.Flags().GetString("password") + if errp != nil || password == "" { + return errors.New("Password is required") + } + nickname, _ := command.Flags().GetString("nickname") + firstname, _ := command.Flags().GetString("firstname") + lastname, _ := command.Flags().GetString("lastname") + locale, _ := command.Flags().GetString("locale") + systemAdmin, _ := command.Flags().GetBool("system_admin") + + user := &model.User{ + Username: username, + Email: email, + Password: password, + Nickname: nickname, + FirstName: firstname, + LastName: lastname, + Locale: locale, + } + + if ruser, err := a.CreateUser(user); err != nil { + return errors.New("Unable to create user. Error: " + err.Error()) + } else if systemAdmin { + a.UpdateUserRoles(ruser.Id, "system_user system_admin", false) + } + + CommandPrettyPrintln("Created User") + + return nil +} + +func userInviteCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 2 { + return errors.New("Expected at least two arguments. See help text for details.") + } + + email := args[0] + if !model.IsValidEmail(email) { + return errors.New("Invalid email") + } + + teams := getTeamsFromTeamArgs(a, args[1:]) + for i, team := range teams { + err := inviteUser(a, email, team, args[i+1]) + + if err != nil { + CommandPrintErrorln(err.Error()) + } + } + + return nil +} + +func inviteUser(a *app.App, email string, team *model.Team, teamArg string) error { + invites := []string{email} + if team == nil { + return fmt.Errorf("Can't find team '%v'", teamArg) + } + + a.SendInviteEmails(team, "Administrator", invites, *a.Config().ServiceSettings.SiteURL) + CommandPrettyPrintln("Invites may or may not have been sent.") + + return nil +} + +func resetUserPasswordCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) != 2 { + return errors.New("Expected two arguments. See help text for details.") + } + + user := getUserFromUserArg(a, args[0]) + if user == nil { + return errors.New("Unable to find user '" + args[0] + "'") + } + password := args[1] + + if result := <-a.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(password)); result.Err != nil { + return result.Err + } + + return nil +} + +func updateUserEmailCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) != 2 { + return errors.New("Expected two arguments. See help text for details.") + } + + newEmail := args[1] + + if !model.IsValidEmail(newEmail) { + return errors.New("Invalid email: '" + newEmail + "'") + } + + if len(args) != 2 { + return errors.New("Expected two arguments. See help text for details.") + } + + user := getUserFromUserArg(a, args[0]) + if user == nil { + return errors.New("Unable to find user '" + args[0] + "'") + } + + user.Email = newEmail + _, errUpdate := a.UpdateUser(user, true) + if errUpdate != nil { + return errors.New(errUpdate.Message) + } + + return nil +} + +func resetUserMfaCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Expected at least one argument. See help text for details.") + } + + users := getUsersFromUserArgs(a, args) + + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if err := a.DeactivateMfa(user.Id); err != nil { + return err + } + } + + return nil +} + +func deleteUserCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Expected at least one argument. See help text for details.") + } + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + CommandPrettyPrintln("Are you sure you want to permanently delete the specified users? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + users := getUsersFromUserArgs(a, args) + + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if err := a.PermanentDeleteUser(user); err != nil { + return err + } + } + + return nil +} + +func deleteAllUsersCommandF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) > 0 { + return errors.New("Expected zero arguments.") + } + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + CommandPrettyPrintln("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + CommandPrettyPrintln("Are you sure you want to permanently delete all user accounts? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + if err := a.PermanentDeleteAllUsers(); err != nil { + return err + } + + CommandPrettyPrintln("All user accounts successfully deleted.") + + return nil +} + +func migrateAuthCmdF(command *cobra.Command, args []string) error { + if args[1] == "saml" { + return migrateAuthToSamlCmdF(command, args) + } + return migrateAuthToLdapCmdF(command, args) +} + +func migrateAuthToLdapCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + fromAuth := args[0] + matchField := args[2] + + if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "saml") { + return errors.New("Invalid from_auth argument") + } + + // Email auth in Mattermost system is represented by "" + if fromAuth == "email" { + fromAuth = "" + } + + if len(matchField) == 0 || (matchField != "email" && matchField != "username") { + return errors.New("Invalid match_field argument") + } + + forceFlag, _ := command.Flags().GetBool("force") + dryRunFlag, _ := command.Flags().GetBool("dryRun") + + if migrate := a.AccountMigration; migrate != nil { + if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag, dryRunFlag); err != nil { + return errors.New("Error while migrating users: " + err.Error()) + } + + CommandPrettyPrintln("Successfully migrated accounts.") + } + + return nil +} + +func migrateAuthToSamlCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + dryRunFlag, _ := command.Flags().GetBool("dryRun") + autoFlag, _ := command.Flags().GetBool("auto") + + matchesFile := "" + matches := map[string]string{} + if !autoFlag { + matchesFile = args[2] + + file, e := ioutil.ReadFile(matchesFile) + if e != nil { + return errors.New("Invalid users file.") + } + if json.Unmarshal(file, &matches) != nil { + return errors.New("Invalid users file.") + } + } + + fromAuth := args[0] + + if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "ldap") { + return errors.New("Invalid from_auth argument") + } + + if autoFlag && !dryRunFlag { + var confirm string + CommandPrettyPrintln("You are about to perform an automatic \"" + fromAuth + " to saml\" migration. This must only be done if your current Mattermost users with " + fromAuth + " auth have the same username and email in your SAML service. Otherwise, provide the usernames and emails from your SAML Service using the \"users file\" without the \"--auto\" option.\n\nDo you want to proceed with automatic migration anyway? (YES/NO):") + fmt.Scanln(&confirm) + + if confirm != "YES" { + return errors.New("ABORTED: You did not answer YES exactly, in all capitals.") + } + } + + // Email auth in Mattermost system is represented by "" + if fromAuth == "email" { + fromAuth = "" + } + + if migrate := a.AccountMigration; migrate != nil { + if err := migrate.MigrateToSaml(fromAuth, matches, autoFlag, dryRunFlag); err != nil { + return errors.New("Error while migrating users: " + err.Error()) + } + + CommandPrettyPrintln("Successfully migrated accounts.") + } + + return nil +} + +func verifyUserCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Expected at least one argument. See help text for details.") + } + + users := getUsersFromUserArgs(a, args) + + for i, user := range users { + if user == nil { + CommandPrintErrorln("Unable to find user '" + args[i] + "'") + continue + } + if cresult := <-a.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil { + CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error()) + } + } + + return nil +} + +func searchUserCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + if len(args) < 1 { + return errors.New("Expected at least one argument. See help text for details.") + } + + users := getUsersFromUserArgs(a, args) + + for i, user := range users { + if i > 0 { + CommandPrettyPrintln("------------------------------") + } + if user == nil { + CommandPrintErrorln("Unable to find user '" + args[i] + "'") + continue + } + + CommandPrettyPrintln("id: " + user.Id) + CommandPrettyPrintln("username: " + user.Username) + CommandPrettyPrintln("nickname: " + user.Nickname) + CommandPrettyPrintln("position: " + user.Position) + CommandPrettyPrintln("first_name: " + user.FirstName) + CommandPrettyPrintln("last_name: " + user.LastName) + CommandPrettyPrintln("email: " + user.Email) + CommandPrettyPrintln("auth_service: " + user.AuthService) + } + + return nil +} diff --git a/cmd/mattermost/commands/user_test.go b/cmd/mattermost/commands/user_test.go new file mode 100644 index 000000000..69ca9ecb8 --- /dev/null +++ b/cmd/mattermost/commands/user_test.go @@ -0,0 +1,121 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "testing" + + "github.com/mattermost/mattermost-server/api4" + "github.com/mattermost/mattermost-server/model" + "github.com/stretchr/testify/require" +) + +func TestCreateUserWithTeam(t *testing.T) { + th := api4.Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + + id := model.NewId() + email := "success+" + id + "@simulator.amazonses.com" + username := "name" + id + + CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) + + CheckCommand(t, "team", "add", th.BasicTeam.Id, email) + + profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetUsersInTeam(th.BasicTeam.Id, 0, 1000, "")).([]*model.User) + + found := false + + for _, user := range profiles { + if user.Email == email { + found = true + } + + } + + if !found { + t.Fatal("Failed to create User") + } +} + +func TestCreateUserWithoutTeam(t *testing.T) { + th := api4.Setup() + defer th.TearDown() + + id := model.NewId() + email := "success+" + id + "@simulator.amazonses.com" + username := "name" + id + + CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) + + if result := <-th.App.Srv.Store.User().GetByEmail(email); result.Err != nil { + t.Fatal() + } else { + user := result.Data.(*model.User) + if user.Email != email { + t.Fatal() + } + } +} + +func TestResetPassword(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + CheckCommand(t, "user", "password", th.BasicUser.Email, "password2") + + th.Client.Logout() + th.BasicUser.Password = "password2" + th.LoginBasic() +} + +func TestMakeUserActiveAndInactive(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + // first inactivate the user + CheckCommand(t, "user", "deactivate", th.BasicUser.Email) + + // activate the inactive user + CheckCommand(t, "user", "activate", th.BasicUser.Email) +} + +func TestChangeUserEmail(t *testing.T) { + th := api4.Setup().InitBasic() + defer th.TearDown() + + newEmail := model.NewId() + "@mattermost-test.com" + + CheckCommand(t, "user", "email", th.BasicUser.Username, newEmail) + if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err == nil { + t.Fatal("should've updated to the new email") + } + if result := <-th.App.Srv.Store.User().GetByEmail(newEmail); result.Err != nil { + t.Fatal() + } else { + user := result.Data.(*model.User) + if user.Email != newEmail { + t.Fatal("should've updated to the new email") + } + } + + // should fail because using an invalid email + require.Error(t, RunCommand(t, "user", "email", th.BasicUser.Username, "wrong$email.com")) + + // should fail because missing one parameter + require.Error(t, RunCommand(t, "user", "email", th.BasicUser.Username)) + + // should fail because missing both parameters + require.Error(t, RunCommand(t, "user", "email")) + + // should fail because have more than 2 parameters + require.Error(t, RunCommand(t, "user", "email", th.BasicUser.Username, "new@email.com", "extra!")) + + // should fail because user not found + require.Error(t, RunCommand(t, "user", "email", "invalidUser", newEmail)) + + // should fail because email already in use + require.Error(t, RunCommand(t, "user", "email", th.BasicUser.Username, th.BasicUser2.Email)) + +} diff --git a/cmd/mattermost/commands/userargs.go b/cmd/mattermost/commands/userargs.go new file mode 100644 index 000000000..ddeed6460 --- /dev/null +++ b/cmd/mattermost/commands/userargs.go @@ -0,0 +1,39 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" +) + +func getUsersFromUserArgs(a *app.App, userArgs []string) []*model.User { + users := make([]*model.User, 0, len(userArgs)) + for _, userArg := range userArgs { + user := getUserFromUserArg(a, userArg) + users = append(users, user) + } + return users +} + +func getUserFromUserArg(a *app.App, userArg string) *model.User { + var user *model.User + if result := <-a.Srv.Store.User().GetByEmail(userArg); result.Err == nil { + user = result.Data.(*model.User) + } + + if user == nil { + if result := <-a.Srv.Store.User().GetByUsername(userArg); result.Err == nil { + user = result.Data.(*model.User) + } + } + + if user == nil { + if result := <-a.Srv.Store.User().Get(userArg); result.Err == nil { + user = result.Data.(*model.User) + } + } + + return user +} diff --git a/cmd/mattermost/commands/version.go b/cmd/mattermost/commands/version.go new file mode 100644 index 000000000..e26d5e09c --- /dev/null +++ b/cmd/mattermost/commands/version.go @@ -0,0 +1,44 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "github.com/mattermost/mattermost-server/app" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" + "github.com/mattermost/mattermost-server/store/sqlstore" + "github.com/spf13/cobra" +) + +var VersionCmd = &cobra.Command{ + Use: "version", + Short: "Display version information", + RunE: versionCmdF, +} + +func init() { + RootCmd.AddCommand(VersionCmd) +} + +func versionCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + + printVersion(a) + + return nil +} + +func printVersion(a *app.App) { + CommandPrintln("Version: " + model.CurrentVersion) + CommandPrintln("Build Number: " + model.BuildNumber) + CommandPrintln("Build Date: " + model.BuildDate) + CommandPrintln("Build Hash: " + model.BuildHash) + CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady) + if supplier, ok := a.Srv.Store.(*store.LayeredStore).DatabaseLayer.(*sqlstore.SqlSupplier); ok { + CommandPrintln("DB Version: " + supplier.GetCurrentSchemaVersion()) + } +} diff --git a/cmd/mattermost/commands/version_test.go b/cmd/mattermost/commands/version_test.go new file mode 100644 index 000000000..0ed2c8ec5 --- /dev/null +++ b/cmd/mattermost/commands/version_test.go @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "testing" +) + +func TestVersion(t *testing.T) { + CheckCommand(t, "version") +} diff --git a/cmd/mattermost/main.go b/cmd/mattermost/main.go new file mode 100644 index 000000000..c74ca8763 --- /dev/null +++ b/cmd/mattermost/main.go @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package main + +import ( + "os" + + "github.com/mattermost/mattermost-server/cmd/mattermost/commands" + + // Plugins + _ "github.com/mattermost/mattermost-server/model/gitlab" + + // Enterprise Imports + _ "github.com/mattermost/mattermost-server/imports" + + // Enterprise Deps + _ "github.com/dgryski/dgoogauth" + _ "github.com/go-ldap/ldap" + _ "github.com/hako/durafmt" + _ "github.com/hashicorp/memberlist" + _ "github.com/mattermost/rsc/qr" + _ "github.com/prometheus/client_golang/prometheus" + _ "github.com/prometheus/client_golang/prometheus/promhttp" + _ "github.com/tylerb/graceful" + _ "gopkg.in/olivere/elastic.v5" +) + +func main() { + if err := commands.Run(os.Args[1:]); err != nil { + os.Exit(1) + } +} diff --git a/cmd/output.go b/cmd/output.go deleted file mode 100644 index 630e831de..000000000 --- a/cmd/output.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package cmd - -import ( - "fmt" - "os" -) - -func CommandPrintln(a ...interface{}) (int, error) { - return fmt.Println(a...) -} - -func CommandPrintErrorln(a ...interface{}) (int, error) { - return fmt.Fprintln(os.Stderr, a...) -} - -func CommandPrettyPrintln(a ...interface{}) (int, error) { - return fmt.Fprintln(os.Stderr, a...) -} diff --git a/cmd/platform/main.go b/cmd/platform/main.go new file mode 100644 index 000000000..b5ea51920 --- /dev/null +++ b/cmd/platform/main.go @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package main + +import ( + "fmt" + "os" + "path/filepath" + "syscall" +) + +func findMattermostBinary() string { + for _, file := range []string{"./mattermost", "../mattermost", "./bin/mattermost"} { + path, _ := filepath.Abs(file) + if stat, err := os.Stat(path); err == nil && !stat.IsDir() { + return path + } + } + return "./mattermost" +} + +func main() { + // Print angry message to use mattermost command directly + fmt.Println(` +------------------------------------ ERROR ------------------------------------------------ +The platform binary has been deprecated, please switch to using the new mattermost binary. +The platform binary will be removed in a future version. +------------------------------------------------------------------------------------------- + `) + + // Execve the real MM binary + args := os.Args + args[0] = "mattermost" + args = append(args, "--platform") + if err := syscall.Exec(findMattermostBinary(), args, nil); err != nil { + fmt.Println("Could not start Mattermost, use the mattermost command directly.") + } +} -- cgit v1.2.3-1-g7c22