From 026553e4f87bfc647a5c03129752e30fc523fa07 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Tue, 6 Dec 2016 10:49:34 -0500 Subject: Improving command line interface (#4689) --- cmd/platform/channel.go | 284 +++++++++++ cmd/platform/channelargs.go | 59 +++ cmd/platform/import.go | 62 +++ cmd/platform/init.go | 41 ++ cmd/platform/ldap.go | 39 ++ cmd/platform/license.go | 50 ++ cmd/platform/mattermost.go | 84 ++++ cmd/platform/oldcommands.go | 1102 +++++++++++++++++++++++++++++++++++++++++++ cmd/platform/output.go | 32 ++ cmd/platform/roles.go | 78 +++ cmd/platform/server.go | 277 +++++++++++ cmd/platform/team.go | 205 ++++++++ cmd/platform/teamargs.go | 32 ++ cmd/platform/test.go | 82 ++++ cmd/platform/user.go | 432 +++++++++++++++++ cmd/platform/userargs.go | 38 ++ cmd/platform/version.go | 30 ++ 17 files changed, 2927 insertions(+) create mode 100644 cmd/platform/channel.go create mode 100644 cmd/platform/channelargs.go create mode 100644 cmd/platform/import.go create mode 100644 cmd/platform/init.go create mode 100644 cmd/platform/ldap.go create mode 100644 cmd/platform/license.go create mode 100644 cmd/platform/mattermost.go create mode 100644 cmd/platform/oldcommands.go create mode 100644 cmd/platform/output.go create mode 100644 cmd/platform/roles.go create mode 100644 cmd/platform/server.go create mode 100644 cmd/platform/team.go create mode 100644 cmd/platform/teamargs.go create mode 100644 cmd/platform/test.go create mode 100644 cmd/platform/user.go create mode 100644 cmd/platform/userargs.go create mode 100644 cmd/platform/version.go (limited to 'cmd') diff --git a/cmd/platform/channel.go b/cmd/platform/channel.go new file mode 100644 index 000000000..cf5ef61bc --- /dev/null +++ b/cmd/platform/channel.go @@ -0,0 +1,284 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "errors" + + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "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 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 mychannel user@example.com username", + RunE: addChannelUsersCmdF, +} + +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 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, +} + +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.") + + channelCmd.AddCommand( + channelCreateCmd, + removeChannelUsersCmd, + addChannelUsersCmd, + deleteChannelsCmd, + listChannelsCmd, + restoreChannelsCmd, + ) +} + +func createChannelCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if !utils.IsLicensed { + return errors.New(utils.T("cli.license.critical")) + } + + name, errn := cmd.Flags().GetString("name") + if errn != nil || name == "" { + return errors.New("Name is required") + } + displayname, errdn := cmd.Flags().GetString("display_name") + if errdn != nil || displayname == "" { + return errors.New("Display Name is required") + } + teamArg, errteam := cmd.Flags().GetString("team") + if errteam != nil || teamArg == "" { + return errors.New("Team is required") + } + header, _ := cmd.Flags().GetString("header") + purpose, _ := cmd.Flags().GetString("purpose") + useprivate, _ := cmd.Flags().GetBool("private") + + channelType := model.CHANNEL_OPEN + if useprivate { + channelType = model.CHANNEL_PRIVATE + } + + team := getTeamFromTeamArg(teamArg) + + channel := &model.Channel{ + TeamId: team.Id, + Name: name, + DisplayName: displayname, + Header: header, + Purpose: purpose, + Type: channelType, + } + + c := getMockContext() + if _, err := api.CreateChannel(c, channel, false); err != nil { + return err + } + + return nil +} + +func removeChannelUsersCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if !utils.IsLicensed { + return errors.New(utils.T("cli.license.critical")) + } + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + channel := getChannelFromChannelArg(args[0]) + if channel == nil { + return errors.New("Unable to find channel '" + args[0] + "'") + } + + users := getUsersFromUserArgs(args[1:]) + for i, user := range users { + removeUserFromChannel(channel, user, args[i+1]) + } + + return nil +} + +func removeUserFromChannel(channel *model.Channel, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if err := api.RemoveUserFromChannel(user.Id, "", channel); err != nil { + CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) + } +} + +func addChannelUsersCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if !utils.IsLicensed { + return errors.New(utils.T("cli.license.critical")) + } + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + channel := getChannelFromChannelArg(args[0]) + if channel == nil { + return errors.New("Unable to find channel '" + args[0] + "'") + } + + users := getUsersFromUserArgs(args[1:]) + for i, user := range users { + addUserToChannel(channel, user, args[i+1]) + } + + return nil +} + +func addUserToChannel(channel *model.Channel, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if _, err := api.AddUserToChannel(user, channel); err != nil { + CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error()) + } +} + +func deleteChannelsCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) < 1 { + return errors.New("Enter at least one channel to delete.") + } + + channels := getChannelsFromChannelArgs(args) + for i, channel := range channels { + if channel == nil { + CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + continue + } + if result := <-api.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil { + CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + result.Err.Error()) + } + } + + return nil +} + +func listChannelsCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if !utils.IsLicensed { + return errors.New(utils.T("cli.license.critical")) + } + + if len(args) < 1 { + return errors.New("Enter at least one team.") + } + + teams := getTeamsFromTeamArgs(args) + for i, team := range teams { + if team == nil { + CommandPrintErrorln("Unable to find team '" + args[i] + "'") + continue + } + if result := <-api.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(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if !utils.IsLicensed { + return errors.New(utils.T("cli.license.critical")) + } + + if len(args) < 1 { + return errors.New("Enter at least one channel.") + } + + channels := getChannelsFromChannelArgs(args) + for i, channel := range channels { + if channel == nil { + CommandPrintErrorln("Unable to find channel '" + args[i] + "'") + continue + } + if result := <-api.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { + CommandPrintErrorln("Unable to restore channel '" + args[i] + "'") + } + } + + return nil +} diff --git a/cmd/platform/channelargs.go b/cmd/platform/channelargs.go new file mode 100644 index 000000000..136d73fd2 --- /dev/null +++ b/cmd/platform/channelargs.go @@ -0,0 +1,59 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "fmt" + "strings" + + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/model" +) + +const CHANNEL_ARG_SEPARATOR = ":" + +func getChannelsFromChannelArgs(channelArgs []string) []*model.Channel { + channels := make([]*model.Channel, 0, len(channelArgs)) + for _, channelArg := range channelArgs { + channel := getChannelFromChannelArg(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(channelArg string) *model.Channel { + teamArg, channelPart := parseChannelArg(channelArg) + if teamArg == "" && channelPart == "" { + return nil + } + + var channel *model.Channel + if teamArg != "" { + team := getTeamFromTeamArg(teamArg) + if team == nil { + return nil + } + + if result := <-api.Srv.Store.Channel().GetByNameIncludeDeleted(team.Id, channelPart); result.Err == nil { + channel = result.Data.(*model.Channel) + } else { + fmt.Println(result.Err.Error()) + } + } + + if channel == nil { + if result := <-api.Srv.Store.Channel().Get(channelPart); result.Err == nil { + channel = result.Data.(*model.Channel) + } + } + + return channel +} diff --git a/cmd/platform/import.go b/cmd/platform/import.go new file mode 100644 index 000000000..b482cda7e --- /dev/null +++ b/cmd/platform/import.go @@ -0,0 +1,62 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "errors" + "os" + + "github.com/mattermost/platform/api" + "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, +} + +func init() { + importCmd.AddCommand( + slackImportCmd, + ) +} + +func slackImportCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) != 2 { + return errors.New("Incorrect number of arguments.") + } + + team := getTeamFromTeamArg(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.") + + api.SlackImport(fileReader, fileInfo.Size(), team.Id) + + CommandPrettyPrintln("Finished Slack Import.") + + return nil +} diff --git a/cmd/platform/init.go b/cmd/platform/init.go new file mode 100644 index 000000000..7e07cce8a --- /dev/null +++ b/cmd/platform/init.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/utils" + "github.com/spf13/cobra" +) + +func doLoadConfig(filename string) (err string) { + defer func() { + if r := recover(); r != nil { + err = fmt.Sprintf("%v", r) + } + }() + utils.TranslationsPreInit() + utils.LoadConfig(filename) + return "" +} + +func initDBCommandContextCobra(cmd *cobra.Command) error { + config, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + initDBCommandContext(config) + + return nil +} + +func initDBCommandContext(configFileLocation string) { + if errstr := doLoadConfig(configFileLocation); errstr != "" { + return + } + + utils.ConfigureCmdLineLog() + + api.NewServer() + api.InitStores() +} diff --git a/cmd/platform/ldap.go b/cmd/platform/ldap.go new file mode 100644 index 000000000..8f3aa0c81 --- /dev/null +++ b/cmd/platform/ldap.go @@ -0,0 +1,39 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "github.com/mattermost/platform/einterfaces" + "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, +} + +func init() { + ldapCmd.AddCommand( + ldapSyncCmd, + ) +} + +func ldapSyncCmdF(cmd *cobra.Command, args []string) error { + if ldapI := einterfaces.GetLdapInterface(); ldapI != nil { + if err := ldapI.Syncronize(); err != nil { + CommandPrintErrorln("ERROR: AD/LDAP Synchronization Failed") + } else { + CommandPrettyPrintln("SUCCESS: AD/LDAP Synchronization Complete") + } + } + + return nil +} diff --git a/cmd/platform/license.go b/cmd/platform/license.go new file mode 100644 index 000000000..776f62856 --- /dev/null +++ b/cmd/platform/license.go @@ -0,0 +1,50 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "errors" + "io/ioutil" + + "github.com/mattermost/platform/api" + "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) +} + +func uploadLicenseCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) != 1 { + return errors.New("Enter one license file to upload") + } + + var fileBytes []byte + var err error + if fileBytes, err = ioutil.ReadFile(args[0]); err != nil { + return err + } + + if _, err := api.SaveLicense(fileBytes); err != nil { + return err + } + + CommandPrettyPrintln("Uploaded license file") + + return nil +} diff --git a/cmd/platform/mattermost.go b/cmd/platform/mattermost.go new file mode 100644 index 000000000..d8210b012 --- /dev/null +++ b/cmd/platform/mattermost.go @@ -0,0 +1,84 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package main + +import ( + "errors" + "flag" + "fmt" + "os" + + "github.com/mattermost/platform/api" + "github.com/spf13/cobra" + + // Plugins + _ "github.com/mattermost/platform/model/gitlab" + + // Enterprise Deps + _ "github.com/dgryski/dgoogauth" + _ "github.com/go-ldap/ldap" + _ "github.com/mattermost/rsc/qr" +) + +//ENTERPRISE_IMPORTS + +func main() { + 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`, + RunE: runServerCmd, + } + rootCmd.PersistentFlags().StringP("config", "c", "config.json", "Configuration file to use.") + + resetCmd.Flags().Bool("confirm", false, "Confirm you really want to delete everything and a DB backup has been performed.") + + rootCmd.AddCommand(serverCmd, versionCmd, userCmd, teamCmd, licenseCmd, importCmd, resetCmd, channelCmd, rolesCmd, testCmd, ldapCmd) + + flag.Usage = func() { + rootCmd.Usage() + } + parseCmds() + + if flagRunCmds { + CommandPrintErrorln("---------------------------------------------------------------------------------------------") + CommandPrintErrorln("DEPRECATED! All previous commands are now deprecated. Run: platform help to see the new ones.") + CommandPrintErrorln("---------------------------------------------------------------------------------------------") + doLegacyCommands() + } else { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } + } +} + +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 resetCmdF(cmd *cobra.Command, args []string) error { + confirmFlag, _ := cmd.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.") + } + } + + api.Srv.Store.DropAllTables() + CommandPrettyPrintln("Database sucessfully reset") + + return nil +} diff --git a/cmd/platform/oldcommands.go b/cmd/platform/oldcommands.go new file mode 100644 index 000000000..1cf898882 --- /dev/null +++ b/cmd/platform/oldcommands.go @@ -0,0 +1,1102 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" + "github.com/mattermost/platform/web" +) + +var flagCmdUpdateDb30 bool +var flagCmdCreateTeam bool +var flagCmdCreateUser bool +var flagCmdInviteUser bool +var flagCmdAssignRole bool +var flagCmdCreateChannel bool +var flagCmdJoinChannel bool +var flagCmdLeaveChannel bool +var flagCmdListChannels bool +var flagCmdRestoreChannel bool +var flagCmdJoinTeam bool +var flagCmdLeaveTeam bool +var flagCmdVersion bool +var flagCmdRunWebClientTests bool +var flagCmdRunJavascriptClientTests bool +var flagCmdResetPassword bool +var flagCmdResetMfa bool +var flagCmdPermanentDeleteUser bool +var flagCmdPermanentDeleteTeam bool +var flagCmdPermanentDeleteAllUsers bool +var flagCmdResetDatabase bool +var flagCmdRunLdapSync bool +var flagCmdMigrateAccounts bool +var flagCmdActivateUser bool +var flagCmdSlackImport bool +var flagUsername string +var flagCmdUploadLicense bool +var flagConfigFile string +var flagLicenseFile string +var flagEmail string +var flagPassword string +var flagTeamName string +var flagChannelName string +var flagConfirmBackup string +var flagRole string +var flagRunCmds bool +var flagFromAuth string +var flagToAuth string +var flagMatchField string +var flagChannelType string +var flagChannelHeader string +var flagChannelPurpose string +var flagUserSetInactive bool +var flagImportArchive string + +func doLegacyCommands() { + doLoadConfig(flagConfigFile) + utils.InitTranslations(utils.Cfg.LocalizationSettings) + utils.ConfigureCmdLineLog() + api.NewServer() + api.InitStores() + api.InitRouter() + api.InitApi() + web.InitWeb() + + if model.BuildEnterpriseReady == "true" { + api.LoadLicense() + } + + runCmds() +} + +func parseCmds() { + flag.StringVar(&flagConfigFile, "config", "config.json", "") + flag.StringVar(&flagUsername, "username", "", "") + flag.StringVar(&flagLicenseFile, "license", "", "") + flag.StringVar(&flagEmail, "email", "", "") + flag.StringVar(&flagPassword, "password", "", "") + flag.StringVar(&flagTeamName, "team_name", "", "") + flag.StringVar(&flagChannelName, "channel_name", "", "") + flag.StringVar(&flagConfirmBackup, "confirm_backup", "", "") + flag.StringVar(&flagFromAuth, "from_auth", "", "") + flag.StringVar(&flagToAuth, "to_auth", "", "") + flag.StringVar(&flagMatchField, "match_field", "email", "") + flag.StringVar(&flagRole, "role", "", "") + flag.StringVar(&flagChannelType, "channel_type", "O", "") + flag.StringVar(&flagChannelHeader, "channel_header", "", "") + flag.StringVar(&flagChannelPurpose, "channel_purpose", "", "") + flag.StringVar(&flagImportArchive, "import_archive", "", "") + + flag.BoolVar(&flagCmdUpdateDb30, "upgrade_db_30", false, "") + flag.BoolVar(&flagCmdCreateTeam, "create_team", false, "") + flag.BoolVar(&flagCmdCreateUser, "create_user", false, "") + flag.BoolVar(&flagCmdInviteUser, "invite_user", false, "") + flag.BoolVar(&flagCmdAssignRole, "assign_role", false, "") + flag.BoolVar(&flagCmdCreateChannel, "create_channel", false, "") + flag.BoolVar(&flagCmdJoinChannel, "join_channel", false, "") + flag.BoolVar(&flagCmdLeaveChannel, "leave_channel", false, "") + flag.BoolVar(&flagCmdListChannels, "list_channels", false, "") + flag.BoolVar(&flagCmdRestoreChannel, "restore_channel", false, "") + flag.BoolVar(&flagCmdJoinTeam, "join_team", false, "") + flag.BoolVar(&flagCmdLeaveTeam, "leave_team", false, "") + flag.BoolVar(&flagCmdVersion, "version", false, "") + flag.BoolVar(&flagCmdRunWebClientTests, "run_web_client_tests", false, "") + flag.BoolVar(&flagCmdRunJavascriptClientTests, "run_javascript_client_tests", false, "") + flag.BoolVar(&flagCmdResetPassword, "reset_password", false, "") + flag.BoolVar(&flagCmdResetMfa, "reset_mfa", false, "") + flag.BoolVar(&flagCmdPermanentDeleteUser, "permanent_delete_user", false, "") + flag.BoolVar(&flagCmdPermanentDeleteTeam, "permanent_delete_team", false, "") + flag.BoolVar(&flagCmdPermanentDeleteAllUsers, "permanent_delete_all_users", false, "") + flag.BoolVar(&flagCmdResetDatabase, "reset_database", false, "") + flag.BoolVar(&flagCmdRunLdapSync, "ldap_sync", false, "") + flag.BoolVar(&flagCmdMigrateAccounts, "migrate_accounts", false, "") + flag.BoolVar(&flagCmdUploadLicense, "upload_license", false, "") + flag.BoolVar(&flagCmdActivateUser, "activate_user", false, "") + flag.BoolVar(&flagCmdSlackImport, "slack_import", false, "") + flag.BoolVar(&flagUserSetInactive, "inactive", false, "") + + flag.Parse() + + flagRunCmds = (flagCmdCreateTeam || + flagCmdCreateUser || + flagCmdInviteUser || + flagCmdLeaveTeam || + flagCmdAssignRole || + flagCmdCreateChannel || + flagCmdJoinChannel || + flagCmdLeaveChannel || + flagCmdListChannels || + flagCmdRestoreChannel || + flagCmdJoinTeam || + flagCmdResetPassword || + flagCmdResetMfa || + flagCmdVersion || + flagCmdRunWebClientTests || + flagCmdRunJavascriptClientTests || + flagCmdPermanentDeleteUser || + flagCmdPermanentDeleteTeam || + flagCmdPermanentDeleteAllUsers || + flagCmdResetDatabase || + flagCmdRunLdapSync || + flagCmdMigrateAccounts || + flagCmdUploadLicense || + flagCmdActivateUser || + flagCmdSlackImport) +} + +func runCmds() { + cmdVersion() + cmdRunClientTests() + cmdCreateTeam() + cmdCreateUser() + cmdInviteUser() + cmdLeaveTeam() + cmdAssignRole() + cmdCreateChannel() + cmdJoinChannel() + cmdLeaveChannel() + cmdListChannels() + cmdRestoreChannel() + cmdJoinTeam() + cmdResetPassword() + cmdResetMfa() + cmdPermDeleteUser() + cmdPermDeleteTeam() + cmdPermDeleteAllUsers() + cmdResetDatabase() + cmdUploadLicense() + cmdRunLdapSync() + cmdRunMigrateAccounts() + cmdActivateUser() + cmdSlackImport() +} + +func cmdRunClientTests() { + if flagCmdRunWebClientTests { + setupClientTests() + api.StartServer() + runWebClientTests() + api.StopServer() + } +} + +func cmdUpdateDb30() { + if flagCmdUpdateDb30 { + // This command is a no-op for backwards compatibility + flushLogAndExit(0) + } +} + +func cmdCreateTeam() { + if flagCmdCreateTeam { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + c := getMockContext() + + team := &model.Team{} + team.DisplayName = flagTeamName + team.Name = flagTeamName + team.Email = flagEmail + team.Type = model.TEAM_OPEN + + api.CreateTeam(c, team) + if c.Err != nil { + if c.Err.Id != "store.sql_team.save.domain_exists.app_error" { + l4g.Error("%v", c.Err) + flushLogAndExit(1) + } + } + + os.Exit(0) + } +} + +func cmdCreateUser() { + if flagCmdCreateUser { + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + if len(flagPassword) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -password") + os.Exit(1) + } + + var team *model.Team + user := &model.User{} + user.Email = flagEmail + user.Password = flagPassword + + if len(flagUsername) == 0 { + splits := strings.Split(strings.Replace(flagEmail, "@", " ", -1), " ") + user.Username = splits[0] + } else { + user.Username = flagUsername + } + + if len(flagTeamName) > 0 { + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + } + + ruser, err := api.CreateUser(user) + if err != nil { + if err.Id != "store.sql_user.save.email_exists.app_error" { + l4g.Error("%v", err) + flushLogAndExit(1) + } + } + + if team != nil { + err = api.JoinUserToTeam(team, ruser) + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + } + + os.Exit(0) + } +} + +func cmdInviteUser() { + if flagCmdInviteUser { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + if len(*utils.Cfg.ServiceSettings.SiteURL) == 0 { + fmt.Fprintln(os.Stderr, "SiteURL must be specified in config.json") + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(team.Email); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + invites := []string{flagEmail} + api.InviteMembers(team, user.GetDisplayName(), invites) + + os.Exit(0) + } +} + +func cmdVersion() { + if flagCmdVersion { + fmt.Fprintln(os.Stderr, "Version: "+model.CurrentVersion) + fmt.Fprintln(os.Stderr, "Build Number: "+model.BuildNumber) + fmt.Fprintln(os.Stderr, "Build Date: "+model.BuildDate) + fmt.Fprintln(os.Stderr, "Build Hash: "+model.BuildHash) + fmt.Fprintln(os.Stderr, "Build Enterprise Ready: "+model.BuildEnterpriseReady) + fmt.Fprintln(os.Stderr, "DB Version: "+api.Srv.Store.(*store.SqlStore).SchemaVersion) + + os.Exit(0) + } +} + +func cmdAssignRole() { + if flagCmdAssignRole { + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + // Do some conversions + if flagRole == "system_admin" { + flagRole = "system_user system_admin" + } + + if flagRole == "" { + flagRole = "system_user" + } + + if !model.IsValidUserRoles(flagRole) { + fmt.Fprintln(os.Stderr, "flag invalid argument: -role") + os.Exit(1) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + if !user.IsInRole(flagRole) { + api.UpdateUserRoles(user, flagRole) + } + + os.Exit(0) + } +} + +func cmdCreateChannel() { + if flagCmdCreateChannel { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagChannelName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -channel_name") + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + if flagChannelType != "O" && flagChannelType != "P" { + fmt.Fprintln(os.Stderr, "flag channel_type must have on of the following values: O or P") + os.Exit(1) + } + + if !utils.IsLicensed { + fmt.Fprintln(os.Stderr, utils.T("cli.license.critical")) + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v %v", utils.T(result.Err.Message), result.Err.DetailedError) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v %v", utils.T(result.Err.Message), result.Err.DetailedError) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + c := getMockContext() + c.Session.UserId = user.Id + + channel := &model.Channel{} + channel.DisplayName = flagChannelName + channel.CreatorId = user.Id + channel.Name = flagChannelName + channel.TeamId = team.Id + channel.Type = flagChannelType + channel.Header = flagChannelHeader + channel.Purpose = flagChannelPurpose + + if _, err := api.CreateChannel(c, channel, true); err != nil { + l4g.Error("%v %v", utils.T(err.Message), err.DetailedError) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdJoinChannel() { + if flagCmdJoinChannel { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + if len(flagChannelName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -channel_name") + os.Exit(1) + } + + if !utils.IsLicensed { + fmt.Fprintln(os.Stderr, utils.T("cli.license.critical")) + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + var channel *model.Channel + if result := <-api.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + channel = result.Data.(*model.Channel) + } + + _, err := api.AddUserToChannel(user, channel) + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdLeaveChannel() { + if flagCmdLeaveChannel { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + if len(flagChannelName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -channel_name") + os.Exit(1) + } + + if flagChannelName == model.DEFAULT_CHANNEL { + fmt.Fprintln(os.Stderr, "flag has invalid argument: -channel_name (cannot leave town-square)") + os.Exit(1) + } + + if !utils.IsLicensed { + fmt.Fprintln(os.Stderr, utils.T("cli.license.critical")) + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + var channel *model.Channel + if result := <-api.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + channel = result.Data.(*model.Channel) + } + + err := api.RemoveUserFromChannel(user.Id, user.Id, channel) + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdListChannels() { + if flagCmdListChannels { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if !utils.IsLicensed { + fmt.Fprintln(os.Stderr, utils.T("cli.license.critical")) + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + channels := result.Data.([]*model.Channel) + + for _, channel := range channels { + + if channel.DeleteAt > 0 { + fmt.Fprintln(os.Stdout, channel.Name+" (archived)") + } else { + fmt.Fprintln(os.Stdout, channel.Name) + } + } + } + + os.Exit(0) + } +} + +func cmdRestoreChannel() { + if flagCmdRestoreChannel { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagChannelName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -channel_name") + os.Exit(1) + } + + if !utils.IsLicensed { + fmt.Fprintln(os.Stderr, utils.T("cli.license.critical")) + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var channel *model.Channel + if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + channels := result.Data.([]*model.Channel) + + for _, ctemp := range channels { + if ctemp.Name == flagChannelName { + channel = ctemp + break + } + } + } + + if result := <-api.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdJoinTeam() { + if flagCmdJoinTeam { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + err := api.JoinUserToTeam(team, user) + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdLeaveTeam() { + if flagCmdLeaveTeam { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + err := api.LeaveTeam(team, user) + + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdResetPassword() { + if flagCmdResetPassword { + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + if len(flagPassword) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -password") + os.Exit(1) + } + + if len(flagPassword) < 5 { + fmt.Fprintln(os.Stderr, "flag invalid argument needs to be more than 4 characters: -password") + os.Exit(1) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + if result := <-api.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(flagPassword)); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdResetMfa() { + if flagCmdResetMfa { + if len(flagEmail) == 0 && len(flagUsername) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email OR -username") + os.Exit(1) + } + + var user *model.User + if len(flagEmail) > 0 { + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + } else { + if result := <-api.Srv.Store.User().GetByUsername(flagUsername); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + } + + if err := api.DeactivateMfa(user.Id); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + os.Exit(0) + } +} + +func cmdPermDeleteUser() { + if flagCmdPermanentDeleteUser { + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + if len(flagConfirmBackup) == 0 { + fmt.Print("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&flagConfirmBackup) + } + + if flagConfirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + var confirm string + fmt.Printf("Are you sure you want to delete the user %v? All data will be permanently deleted? (YES/NO): ", user.Email) + fmt.Scanln(&confirm) + if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + if err := api.PermanentDeleteUser(user); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } else { + fmt.Print("SUCCESS: User deleted.") + flushLogAndExit(0) + } + } +} + +func cmdPermDeleteTeam() { + if flagCmdPermanentDeleteTeam { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + if len(flagConfirmBackup) == 0 { + fmt.Print("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&flagConfirmBackup) + } + + if flagConfirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + var confirm string + fmt.Printf("Are you sure you want to delete the team %v? All data will be permanently deleted? (YES/NO): ", team.Name) + fmt.Scanln(&confirm) + if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + if err := api.PermanentDeleteTeam(team); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } else { + fmt.Print("SUCCESS: Team deleted.") + flushLogAndExit(0) + } + } +} + +func cmdPermDeleteAllUsers() { + if flagCmdPermanentDeleteAllUsers { + if len(flagConfirmBackup) == 0 { + fmt.Print("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&flagConfirmBackup) + } + + if flagConfirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + var confirm string + fmt.Printf("Are you sure you want to delete all the users? All data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + if err := api.PermanentDeleteAllUsers(); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } else { + fmt.Print("SUCCESS: All users deleted.") + flushLogAndExit(0) + } + } +} + +func cmdResetDatabase() { + if flagCmdResetDatabase { + + if len(flagConfirmBackup) == 0 { + fmt.Print("Have you performed a database backup? (YES/NO): ") + fmt.Scanln(&flagConfirmBackup) + } + + if flagConfirmBackup != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + var confirm string + fmt.Printf("Are you sure you want to delete everything? ALL data will be permanently deleted? (YES/NO): ") + fmt.Scanln(&confirm) + if confirm != "YES" { + fmt.Print("ABORTED: You did not answer YES exactly, in all capitals.") + flushLogAndExit(1) + } + + api.Srv.Store.DropAllTables() + fmt.Print("SUCCESS: Database reset.") + flushLogAndExit(0) + } + +} + +func cmdRunLdapSync() { + if flagCmdRunLdapSync { + if ldapI := einterfaces.GetLdapInterface(); ldapI != nil { + if err := ldapI.Syncronize(); err != nil { + fmt.Println("ERROR: AD/LDAP Syncronization Failed") + l4g.Error("%v", err.Error()) + flushLogAndExit(1) + } else { + fmt.Println("SUCCESS: AD/LDAP Syncronization Complete") + flushLogAndExit(0) + } + } + } +} + +func cmdRunMigrateAccounts() { + if flagCmdMigrateAccounts { + if len(flagFromAuth) == 0 || (flagFromAuth != "email" && flagFromAuth != "gitlab" && flagFromAuth != "saml") { + fmt.Fprintln(os.Stderr, "flag needs an argument: -from_auth") + os.Exit(1) + } + + if len(flagToAuth) == 0 || flagToAuth != "ldap" { + fmt.Fprintln(os.Stderr, "flag needs an argument: -from_auth") + os.Exit(1) + } + + // Email auth in Mattermost system is represented by "" + if flagFromAuth == "email" { + flagFromAuth = "" + } + + if len(flagMatchField) == 0 || (flagMatchField != "email" && flagMatchField != "username") { + fmt.Fprintln(os.Stderr, "flag needs an argument: -match_field") + os.Exit(1) + } + + if migrate := einterfaces.GetAccountMigrationInterface(); migrate != nil { + if err := migrate.MigrateToLdap(flagFromAuth, flagMatchField); err != nil { + fmt.Println("ERROR: Account migration failed.") + l4g.Error("%v", err.Error()) + flushLogAndExit(1) + } else { + fmt.Println("SUCCESS: Account migration complete.") + flushLogAndExit(0) + } + } + } +} + +func cmdUploadLicense() { + if flagCmdUploadLicense { + if model.BuildEnterpriseReady != "true" { + fmt.Fprintln(os.Stderr, "build must be enterprise ready") + os.Exit(1) + } + + if len(flagLicenseFile) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + var fileBytes []byte + var err error + if fileBytes, err = ioutil.ReadFile(flagLicenseFile); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + if _, err := api.SaveLicense(fileBytes); err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } else { + flushLogAndExit(0) + } + + flushLogAndExit(0) + } +} + +func cmdActivateUser() { + if flagCmdActivateUser { + if len(flagEmail) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -email") + os.Exit(1) + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + user = result.Data.(*model.User) + } + + if user.IsLDAPUser() { + l4g.Error("%v", utils.T("api.user.update_active.no_deactivate_ldap.app_error")) + } + + if _, err := api.UpdateActive(user, !flagUserSetInactive); err != nil { + l4g.Error("%v", err) + } + + os.Exit(0) + } +} + +func cmdSlackImport() { + if flagCmdSlackImport { + if len(flagTeamName) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name") + os.Exit(1) + } + + if len(flagImportArchive) == 0 { + fmt.Fprintln(os.Stderr, "flag needs an argument: -import_archive") + os.Exit(1) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil { + l4g.Error("%v", result.Err) + flushLogAndExit(1) + } else { + team = result.Data.(*model.Team) + } + + fileReader, err := os.Open(flagImportArchive) + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + defer fileReader.Close() + + fileInfo, err := fileReader.Stat() + if err != nil { + l4g.Error("%v", err) + flushLogAndExit(1) + } + + fmt.Fprintln(os.Stdout, "Running Slack Import. This may take a long time for large teams or teams with many messages.") + + api.SlackImport(fileReader, fileInfo.Size(), team.Id) + + flushLogAndExit(0) + } +} + +func flushLogAndExit(code int) { + l4g.Close() + time.Sleep(time.Second) + os.Exit(code) +} + +func getMockContext() *api.Context { + c := &api.Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + c.T = utils.TfuncWithFallback(model.DEFAULT_LOCALE) + c.Locale = model.DEFAULT_LOCALE + + if *utils.Cfg.ServiceSettings.SiteURL != "" { + c.SetSiteURL(*utils.Cfg.ServiceSettings.SiteURL) + } + + return c +} diff --git a/cmd/platform/output.go b/cmd/platform/output.go new file mode 100644 index 000000000..5b7f91385 --- /dev/null +++ b/cmd/platform/output.go @@ -0,0 +1,32 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "fmt" + "os" +) + +func CommandPrintln(a ...interface{}) (int, error) { + return fmt.Println(a...) +} + +func CommandPrint(a ...interface{}) (int, error) { + return fmt.Print(a...) +} + +func CommandPrintErrorln(a ...interface{}) (int, error) { + return fmt.Fprintln(os.Stderr, a...) +} + +func CommandPrintError(a ...interface{}) (int, error) { + return fmt.Fprint(os.Stderr, a...) +} + +func CommandPrettyPrintln(a ...interface{}) (int, error) { + return fmt.Fprintln(os.Stderr, a...) +} + +func CommandPrettyPrint(a ...interface{}) (int, error) { + return fmt.Fprint(os.Stderr, a...) +} diff --git a/cmd/platform/roles.go b/cmd/platform/roles.go new file mode 100644 index 000000000..7b635c5a3 --- /dev/null +++ b/cmd/platform/roles.go @@ -0,0 +1,78 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "errors" + + "github.com/mattermost/platform/api" + "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, + ) +} + +func makeSystemAdminCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) < 1 { + return errors.New("Enter at least one user.") + } + + users := getUsersFromUserArgs(args) + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if _, err := api.UpdateUserRoles(user, "system_admin system_user"); err != nil { + return err + } + } + + return nil +} + +func makeMemberCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) < 1 { + return errors.New("Enter at least one user.") + } + + users := getUsersFromUserArgs(args) + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if _, err := api.UpdateUserRoles(user, "system_user"); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/platform/server.go b/cmd/platform/server.go new file mode 100644 index 000000000..c5aae960a --- /dev/null +++ b/cmd/platform/server.go @@ -0,0 +1,277 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package main + +import ( + "io/ioutil" + "net/http" + "net/url" + "os" + "os/signal" + "runtime" + "strconv" + "syscall" + "time" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/manualtesting" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "github.com/mattermost/platform/web" + "github.com/spf13/cobra" +) + +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Run the Mattermost server", + RunE: runServerCmd, +} + +func runServerCmd(cmd *cobra.Command, args []string) error { + config, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + runServer(config) + return nil +} + +func runServer(configFileLocation string) { + if errstr := doLoadConfig(configFileLocation); errstr != "" { + l4g.Exit("Unable to load mattermost configuration file: ", errstr) + return + } + + utils.InitTranslations(utils.Cfg.LocalizationSettings) + utils.TestConnection(utils.Cfg) + + pwd, _ := os.Getwd() + l4g.Info(utils.T("mattermost.current_version"), model.CurrentVersion, model.BuildNumber, model.BuildDate, model.BuildHash, model.BuildHashEnterprise) + l4g.Info(utils.T("mattermost.entreprise_enabled"), model.BuildEnterpriseReady) + l4g.Info(utils.T("mattermost.working_dir"), pwd) + l4g.Info(utils.T("mattermost.config_file"), utils.FindConfigFile(configFileLocation)) + + // Enable developer settings if this is a "dev" build + if model.BuildNumber == "dev" { + *utils.Cfg.ServiceSettings.EnableDeveloper = true + } + + cmdUpdateDb30() + + api.NewServer() + api.InitStores() + api.InitRouter() + api.InitApi() + web.InitWeb() + + if model.BuildEnterpriseReady == "true" { + api.LoadLicense() + } + + if !utils.IsLicensed && len(utils.Cfg.SqlSettings.DataSourceReplicas) > 1 { + l4g.Critical(utils.T("store.sql.read_replicas_not_licensed.critical")) + return + } + + resetStatuses() + + api.StartServer() + + // If we allow testing then listen for manual testing URL hits + if utils.Cfg.ServiceSettings.EnableTesting { + manualtesting.InitManualTesting() + } + + setDiagnosticId() + go runSecurityAndDiagnosticsJob() + + if complianceI := einterfaces.GetComplianceInterface(); complianceI != nil { + complianceI.StartComplianceDailyJob() + } + + if einterfaces.GetClusterInterface() != nil { + einterfaces.GetClusterInterface().StartInterNodeCommunication() + } + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().StartServer() + } + + // wait for kill signal before attempting to gracefully shutdown + // the running service + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-c + + if einterfaces.GetClusterInterface() != nil { + einterfaces.GetClusterInterface().StopInterNodeCommunication() + } + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().StopServer() + } + + api.StopServer() +} + +func runSecurityAndDiagnosticsJob() { + doSecurityAndDiagnostics() + model.CreateRecurringTask("Security and Diagnostics", doSecurityAndDiagnostics, time.Hour*4) +} + +func resetStatuses() { + if result := <-api.Srv.Store.Status().ResetAll(); result.Err != nil { + l4g.Error(utils.T("mattermost.reset_status.error"), result.Err.Error()) + } +} + +func setDiagnosticId() { + if result := <-api.Srv.Store.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + + id := props[model.SYSTEM_DIAGNOSTIC_ID] + if len(id) == 0 { + id = model.NewId() + systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id} + <-api.Srv.Store.System().Save(systemId) + } + + utils.CfgDiagnosticId = id + } +} + +func doSecurityAndDiagnostics() { + if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert { + if result := <-api.Srv.Store.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + lastSecurityTime, _ := strconv.ParseInt(props[model.SYSTEM_LAST_SECURITY_TIME], 10, 0) + currentTime := model.GetMillis() + + if (currentTime - lastSecurityTime) > 1000*60*60*24*1 { + l4g.Debug(utils.T("mattermost.security_checks.debug")) + + v := url.Values{} + + v.Set(utils.PROP_DIAGNOSTIC_ID, utils.CfgDiagnosticId) + v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber) + v.Set(utils.PROP_DIAGNOSTIC_ENTERPRISE_READY, model.BuildEnterpriseReady) + v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName) + v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS) + v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT) + + if len(props[model.SYSTEM_RAN_UNIT_TESTS]) > 0 { + v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "1") + } else { + v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "0") + } + + systemSecurityLastTime := &model.System{Name: model.SYSTEM_LAST_SECURITY_TIME, Value: strconv.FormatInt(currentTime, 10)} + if lastSecurityTime == 0 { + <-api.Srv.Store.System().Save(systemSecurityLastTime) + } else { + <-api.Srv.Store.System().Update(systemSecurityLastTime) + } + + if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) + } + + if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil { + v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) + } + + if tcr := <-api.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil { + v.Set(utils.PROP_DIAGNOSTIC_TEAM_COUNT, strconv.FormatInt(tcr.Data.(int64), 10)) + } + + res, err := http.Get(utils.DIAGNOSTIC_URL + "/security?" + v.Encode()) + if err != nil { + l4g.Error(utils.T("mattermost.security_info.error")) + return + } + + bulletins := model.SecurityBulletinsFromJson(res.Body) + ioutil.ReadAll(res.Body) + res.Body.Close() + + for _, bulletin := range bulletins { + if bulletin.AppliesToVersion == model.CurrentVersion { + if props["SecurityBulletin_"+bulletin.Id] == "" { + if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil { + l4g.Error(utils.T("mattermost.system_admins.error")) + return + } else { + users := results.Data.(map[string]*model.User) + + resBody, err := http.Get(utils.DIAGNOSTIC_URL + "/bulletins/" + bulletin.Id) + if err != nil { + l4g.Error(utils.T("mattermost.security_bulletin.error")) + return + } + + body, err := ioutil.ReadAll(resBody.Body) + res.Body.Close() + if err != nil || resBody.StatusCode != 200 { + l4g.Error(utils.T("mattermost.security_bulletin_read.error")) + return + } + + for _, user := range users { + l4g.Info(utils.T("mattermost.send_bulletin.info"), bulletin.Id, user.Email) + utils.SendMail(user.Email, utils.T("mattermost.bulletin.subject"), string(body)) + } + } + + bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id} + <-api.Srv.Store.System().Save(bulletinSeen) + } + } + } + } + } + } + + if *utils.Cfg.LogSettings.EnableDiagnostics { + utils.SendGeneralDiagnostics() + sendServerDiagnostics() + } +} + +func sendServerDiagnostics() { + var userCount int64 + var activeUserCount int64 + var teamCount int64 + + if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + userCount = ucr.Data.(int64) + } + + if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil { + activeUserCount = ucr.Data.(int64) + } + + if tcr := <-api.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil { + teamCount = tcr.Data.(int64) + } + + utils.SendDiagnostic(utils.TRACK_ACTIVITY, map[string]interface{}{ + "registered_users": userCount, + "active_users": activeUserCount, + "teams": teamCount, + }) + + edition := model.BuildEnterpriseReady + version := model.CurrentVersion + database := utils.Cfg.SqlSettings.DriverName + operatingSystem := runtime.GOOS + + utils.SendDiagnostic(utils.TRACK_VERSION, map[string]interface{}{ + "edition": edition, + "version": version, + "database": database, + "operating_system": operatingSystem, + }) +} diff --git a/cmd/platform/team.go b/cmd/platform/team.go new file mode 100644 index 000000000..8fecda6e1 --- /dev/null +++ b/cmd/platform/team.go @@ -0,0 +1,205 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "errors" + "fmt" + + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/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" + teams 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, +} + +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, + ) +} + +func createTeamCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + name, errn := cmd.Flags().GetString("name") + if errn != nil || name == "" { + return errors.New("Name is required") + } + displayname, errdn := cmd.Flags().GetString("display_name") + if errdn != nil || displayname == "" { + return errors.New("Display Name is required") + } + email, _ := cmd.Flags().GetString("email") + useprivate, _ := cmd.Flags().GetBool("private") + + teamType := model.TEAM_OPEN + if useprivate { + teamType = model.TEAM_INVITE + } + + team := &model.Team{ + Name: name, + DisplayName: displayname, + Email: email, + Type: teamType, + } + + c := getMockContext() + api.CreateTeam(c, team) + if c.Err != nil { + return errors.New("Team creation failed: " + c.Err.Error()) + } + + return nil +} + +func removeUsersCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + team := getTeamFromTeamArg(args[0]) + if team == nil { + return errors.New("Unable to find team '" + args[0] + "'") + } + + users := getUsersFromUserArgs(args[1:]) + for i, user := range users { + removeUserFromTeam(team, user, args[i+1]) + } + + return nil +} + +func removeUserFromTeam(team *model.Team, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if err := api.LeaveTeam(team, user); err != nil { + CommandPrintErrorln("Unable to remove '" + userArg + "' from " + team.Name + ". Error: " + err.Error()) + } +} + +func addUsersCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + team := getTeamFromTeamArg(args[0]) + if team == nil { + return errors.New("Unable to find team '" + args[0] + "'") + } + + users := getUsersFromUserArgs(args[1:]) + for i, user := range users { + addUserToTeam(team, user, args[i+1]) + } + + return nil +} + +func addUserToTeam(team *model.Team, user *model.User, userArg string) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if err := api.JoinUserToTeam(team, user); err != nil { + CommandPrintErrorln("Unable to add '" + userArg + "' to " + team.Name) + } +} + +func deleteTeamsCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) < 1 { + return errors.New("Not enough arguments.") + } + + confirmFlag, _ := cmd.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(args) + for i, team := range teams { + if team == nil { + CommandPrintErrorln("Unable to find team '" + args[i] + "'") + continue + } + if err := deleteTeam(team); err != nil { + CommandPrintErrorln("Unable to delete team '" + team.Name + "' error: " + err.Error()) + } else { + CommandPrettyPrintln("Deleted team '" + team.Name + "'") + } + } + + return nil +} + +func deleteTeam(team *model.Team) *model.AppError { + return api.PermanentDeleteTeam(team) +} diff --git a/cmd/platform/teamargs.go b/cmd/platform/teamargs.go new file mode 100644 index 000000000..5ad56f5d9 --- /dev/null +++ b/cmd/platform/teamargs.go @@ -0,0 +1,32 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/model" +) + +func getTeamsFromTeamArgs(teamArgs []string) []*model.Team { + teams := make([]*model.Team, 0, len(teamArgs)) + for _, teamArg := range teamArgs { + team := getTeamFromTeamArg(teamArg) + teams = append(teams, team) + } + return teams +} + +func getTeamFromTeamArg(teamArg string) *model.Team { + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(teamArg); result.Err == nil { + team = result.Data.(*model.Team) + } + + if team == nil { + if result := <-api.Srv.Store.Team().Get(teamArg); result.Err == nil { + team = result.Data.(*model.Team) + } + } + + return team +} diff --git a/cmd/platform/test.go b/cmd/platform/test.go new file mode 100644 index 000000000..d82734c75 --- /dev/null +++ b/cmd/platform/test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/utils" + "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, +} + +func init() { + testCmd.AddCommand( + runWebClientTestsCmd, + ) +} + +func webClientTestsCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + utils.InitTranslations(utils.Cfg.LocalizationSettings) + api.InitRouter() + api.InitApi() + setupClientTests() + api.StartServer() + runWebClientTests() + api.StopServer() + + return nil +} + +func setupClientTests() { + *utils.Cfg.TeamSettings.EnableOpenServer = true + *utils.Cfg.ServiceSettings.EnableCommands = false + *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false + *utils.Cfg.ServiceSettings.EnableCustomEmoji = true + utils.Cfg.ServiceSettings.EnableIncomingWebhooks = false + utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = false + utils.SetDefaultRolesBasedOnConfig() +} + +func executeTestCommand(cmd *exec.Cmd) { + cmdOutPipe, err := cmd.StdoutPipe() + if err != nil { + CommandPrintErrorln("Failed to run tests") + return + } + + cmdOutReader := bufio.NewScanner(cmdOutPipe) + go func() { + for cmdOutReader.Scan() { + fmt.Println(cmdOutReader.Text()) + } + }() + + if err := cmd.Run(); err != nil { + CommandPrintErrorln("Client Tests failed") + return + } +} + +func runWebClientTests() { + os.Chdir("webapp") + cmd := exec.Command("npm", "test") + executeTestCommand(cmd) +} diff --git a/cmd/platform/user.go b/cmd/platform/user.go new file mode 100644 index 000000000..43a00e68d --- /dev/null +++ b/cmd/platform/user.go @@ -0,0 +1,432 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "errors" + "fmt" + + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "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 + user create --firstname Joe --system_admin --email joe@example.com --username joe --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 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: deleteUserCmdF, +} + +var migrateAuthCmd = &cobra.Command{ + Use: "migrate_auth [from_auth] [to_auth] [match_field]", + 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. + +from_auth: + The authentication service to migrate users accounts from. + Supported options: email, gitlab, saml. + +to_auth: + The authentication service to migrate users to. + Supported 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. + +Will display any accounts that are not migrated successfully.`, + Example: " user migrate_auth email ladp email", + 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, +} + +func init() { + userCreateCmd.Flags().String("username", "", "Username") + userCreateCmd.Flags().String("email", "", "Email") + userCreateCmd.Flags().String("password", "", "Password") + userCreateCmd.Flags().String("nickname", "", "Nickname") + userCreateCmd.Flags().String("firstname", "", "First Name") + userCreateCmd.Flags().String("lastname", "", "Last Name") + userCreateCmd.Flags().String("locale", "", "Locale (ex: en, fr)") + userCreateCmd.Flags().Bool("system_admin", false, "Make the user a system administrator") + + 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.") + + userCmd.AddCommand( + userActivateCmd, + userDeactivateCmd, + userCreateCmd, + userInviteCmd, + resetUserPasswordCmd, + resetUserMfaCmd, + deleteUserCmd, + deleteAllUsersCmd, + migrateAuthCmd, + verifyUserCmd, + ) +} + +func userActivateCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) < 1 { + return errors.New("Enter user(s) to activate.") + } + + changeUsersActiveStatus(args, true) + return nil +} + +func changeUsersActiveStatus(userArgs []string, active bool) { + users := getUsersFromUserArgs(userArgs) + for i, user := range users { + changeUserActiveStatus(user, userArgs[i], active) + } +} + +func changeUserActiveStatus(user *model.User, userArg string, activate bool) { + if user == nil { + CommandPrintErrorln("Can't find user '" + userArg + "'") + return + } + if user.IsLDAPUser() { + CommandPrintErrorln(utils.T("api.user.update_active.no_deactivate_ldap.app_error")) + return + } + if _, err := api.UpdateActive(user, activate); err != nil { + CommandPrintErrorln("Unable to change activation status of user: " + userArg) + } +} + +func userDeactivateCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + + if len(args) < 1 { + return errors.New("Enter user(s) to deactivate.") + } + + changeUsersActiveStatus(args, false) + return nil +} + +func userCreateCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + username, erru := cmd.Flags().GetString("username") + if erru != nil || username == "" { + return errors.New("Username is required") + } + email, erre := cmd.Flags().GetString("email") + if erre != nil || email == "" { + return errors.New("Email is required") + } + password, errp := cmd.Flags().GetString("password") + if errp != nil || password == "" { + return errors.New("Password is required") + } + nickname, _ := cmd.Flags().GetString("nickname") + firstname, _ := cmd.Flags().GetString("firstname") + lastname, _ := cmd.Flags().GetString("lastname") + locale, _ := cmd.Flags().GetString("locale") + system_admin, _ := cmd.Flags().GetBool("system_admin") + + user := &model.User{ + Username: username, + Email: email, + Password: password, + Nickname: nickname, + FirstName: firstname, + LastName: lastname, + Locale: locale, + } + + ruser, err := api.CreateUser(user) + if err != nil { + return errors.New("Unable to create user. Error: " + err.Error()) + } + + if system_admin { + api.UpdateUserRoles(ruser, "system_user system_admin") + } + + CommandPrettyPrintln("Created User") + + return nil +} + +func userInviteCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + utils.InitHTML() + + if len(args) < 2 { + return errors.New("Not enough arguments.") + } + + email := args[0] + if !model.IsValidEmail(email) { + return errors.New("Invalid email") + } + + teams := getTeamsFromTeamArgs(args[1:]) + for i, team := range teams { + inviteUser(email, team, args[i+1]) + } + + return nil +} + +func inviteUser(email string, team *model.Team, teamArg string) { + invites := []string{email} + if team == nil { + CommandPrintErrorln("Can't find team '" + teamArg + "'") + return + } + api.InviteMembers(team, "Administrator", invites) + CommandPrettyPrintln("Invites may or may not have been sent.") +} + +func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) != 2 { + return errors.New("Incorect number of arguments.") + } + + user := getUserFromUserArg(args[0]) + if user == nil { + return errors.New("Unable to find user '" + args[0] + "'") + } + password := args[1] + + if result := <-api.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(password)); result.Err != nil { + return result.Err + } + + return nil +} + +func resetUserMfaCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) < 1 { + return errors.New("Enter at least one user.") + } + + users := getUsersFromUserArgs(args) + + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if err := api.DeactivateMfa(user.Id); err != nil { + return err + } + } + + return nil +} + +func deleteUserCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) < 1 { + return errors.New("Enter at least one user.") + } + + confirmFlag, _ := cmd.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.") + } + } + + users := getUsersFromUserArgs(args) + + for i, user := range users { + if user == nil { + return errors.New("Unable to find user '" + args[i] + "'") + } + + if err := api.PermanentDeleteUser(user); err != nil { + return err + } + } + + return nil +} + +func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) > 0 { + return errors.New("Don't enter any agruments.") + } + + confirmFlag, _ := cmd.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.") + } + } + + if err := api.PermanentDeleteAllUsers(); err != nil { + return err + } else { + CommandPrettyPrintln("Sucsessfull. All users deleted.") + } + + return nil +} + +func migrateAuthCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) != 3 { + return errors.New("Enter the correct number of arguments.") + } + + fromAuth := args[0] + toAuth := args[1] + matchField := args[2] + + if len(fromAuth) == 0 || (fromAuth != "email" && fromAuth != "gitlab" && fromAuth != "saml") { + return errors.New("Invalid from_auth argument") + } + + if len(toAuth) == 0 || toAuth != "ldap" { + return errors.New("Invalid to_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") + } + + if migrate := einterfaces.GetAccountMigrationInterface(); migrate != nil { + if err := migrate.MigrateToLdap(fromAuth, matchField); err != nil { + return errors.New("Error while migrating users: " + err.Error()) + } else { + CommandPrettyPrintln("Sucessfully migrated accounts.") + } + } + + return nil +} + +func verifyUserCmdF(cmd *cobra.Command, args []string) error { + initDBCommandContextCobra(cmd) + if len(args) < 1 { + return errors.New("Enter at least one user.") + } + + users := getUsersFromUserArgs(args) + + for i, user := range users { + if user == nil { + CommandPrintErrorln("Unable to find user '" + args[i] + "'") + } + if cresult := <-api.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil { + CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error()) + } + } + + return nil +} diff --git a/cmd/platform/userargs.go b/cmd/platform/userargs.go new file mode 100644 index 000000000..9ac00ae70 --- /dev/null +++ b/cmd/platform/userargs.go @@ -0,0 +1,38 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/model" +) + +func getUsersFromUserArgs(userArgs []string) []*model.User { + users := make([]*model.User, 0, len(userArgs)) + for _, userArg := range userArgs { + user := getUserFromUserArg(userArg) + users = append(users, user) + } + return users +} + +func getUserFromUserArg(userArg string) *model.User { + var user *model.User + if result := <-api.Srv.Store.User().GetByEmail(userArg); result.Err == nil { + user = result.Data.(*model.User) + } + + if user == nil { + if result := <-api.Srv.Store.User().GetByUsername(userArg); result.Err == nil { + user = result.Data.(*model.User) + } + } + + if user == nil { + if result := <-api.Srv.Store.User().Get(userArg); result.Err == nil { + user = result.Data.(*model.User) + } + } + + return user +} diff --git a/cmd/platform/version.go b/cmd/platform/version.go new file mode 100644 index 000000000..8da34e3a9 --- /dev/null +++ b/cmd/platform/version.go @@ -0,0 +1,30 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +package main + +import ( + "github.com/mattermost/platform/api" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/spf13/cobra" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Display version information", + Run: versionCmdF, +} + +func versionCmdF(cmd *cobra.Command, args []string) { + initDBCommandContextCobra(cmd) + printVersion() +} + +func printVersion() { + 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) + CommandPrintln("DB Version: " + api.Srv.Store.(*store.SqlStore).SchemaVersion) +} -- cgit v1.2.3-1-g7c22