summaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/platform/channel.go17
-rw-r--r--cmd/platform/channel_test.go27
-rw-r--r--cmd/platform/jobserver.go2
-rw-r--r--cmd/platform/mattermost.go3
-rw-r--r--cmd/platform/server.go71
-rw-r--r--cmd/platform/server_test.go136
-rw-r--r--cmd/platform/test.go12
-rw-r--r--cmd/platform/user.go189
8 files changed, 411 insertions, 46 deletions
diff --git a/cmd/platform/channel.go b/cmd/platform/channel.go
index 98bdcebb8..5d86ad9da 100644
--- a/cmd/platform/channel.go
+++ b/cmd/platform/channel.go
@@ -106,6 +106,8 @@ func init() {
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")
@@ -319,26 +321,33 @@ func moveChannelsCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Unable to find destination team '" + args[0] + "'")
}
+ username, erru := cmd.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
}
- if err := moveChannel(a, team, channel); err != nil {
+ 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 + "'")
+ 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) *model.AppError {
+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); err != nil {
+ if err := a.MoveChannel(team, channel, user); err != nil {
return err
}
diff --git a/cmd/platform/channel_test.go b/cmd/platform/channel_test.go
index 1e6915679..cf8603cf3 100644
--- a/cmd/platform/channel_test.go
+++ b/cmd/platform/channel_test.go
@@ -44,6 +44,33 @@ func TestRemoveChannel(t *testing.T) {
checkCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
}
+func TestMoveChannel(t *testing.T) {
+ th := api.Setup().InitBasic()
+ defer th.TearDown()
+
+ client := th.BasicClient
+ team1 := th.BasicTeam
+ team2 := th.CreateTeam(client)
+ 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 := api.Setup().InitBasic()
defer th.TearDown()
diff --git a/cmd/platform/jobserver.go b/cmd/platform/jobserver.go
index e664136c0..044ee6b6a 100644
--- a/cmd/platform/jobserver.go
+++ b/cmd/platform/jobserver.go
@@ -35,7 +35,7 @@ func jobserverCmdF(cmd *cobra.Command, args []string) {
defer l4g.Close()
defer a.Shutdown()
- a.Jobs.LoadLicense()
+ a.LoadLicense()
// Run jobs
l4g.Info("Starting Mattermost job server")
diff --git a/cmd/platform/mattermost.go b/cmd/platform/mattermost.go
index b0190011b..e4a120e1e 100644
--- a/cmd/platform/mattermost.go
+++ b/cmd/platform/mattermost.go
@@ -25,6 +25,9 @@ import (
_ "github.com/prometheus/client_golang/prometheus/promhttp"
_ "github.com/tylerb/graceful"
_ "gopkg.in/olivere/elastic.v5"
+
+ // Temp imports for new dependencies
+ _ "github.com/gorilla/schema"
)
func main() {
diff --git a/cmd/platform/server.go b/cmd/platform/server.go
index e3742cef6..31606e6eb 100644
--- a/cmd/platform/server.go
+++ b/cmd/platform/server.go
@@ -4,6 +4,7 @@
package main
import (
+ "net"
"os"
"os/signal"
"syscall"
@@ -42,10 +43,11 @@ func runServerCmd(cmd *cobra.Command, args []string) error {
disableConfigWatch, _ := cmd.Flags().GetBool("disableconfigwatch")
- return runServer(config, disableConfigWatch)
+ interruptChan := make(chan os.Signal, 1)
+ return runServer(config, disableConfigWatch, interruptChan)
}
-func runServer(configFileLocation string, disableConfigWatch bool) error {
+func runServer(configFileLocation string, disableConfigWatch bool, interruptChan chan os.Signal) error {
options := []app.Option{app.ConfigFile(configFileLocation)}
if disableConfigWatch {
options = append(options, app.DisableConfigWatch)
@@ -53,7 +55,7 @@ func runServer(configFileLocation string, disableConfigWatch bool) error {
a, err := app.New(options...)
if err != nil {
- l4g.Error(err.Error())
+ l4g.Critical(err.Error())
return err
}
defer a.Shutdown()
@@ -87,7 +89,12 @@ func runServer(configFileLocation string, disableConfigWatch bool) error {
}
})
- a.StartServer()
+ serverErr := a.StartServer()
+ if serverErr != nil {
+ l4g.Critical(serverErr.Error())
+ return serverErr
+ }
+
api4.Init(a, a.Srv.Router, false)
api3 := api.Init(a, a.Srv.Router)
wsapi.Init(a, a.Srv.WebSocketRouter)
@@ -124,11 +131,21 @@ func runServer(configFileLocation string, disableConfigWatch bool) error {
a.EnsureDiagnosticId()
- go runSecurityJob(a)
- go runDiagnosticsJob(a)
- go runSessionCleanupJob(a)
- go runTokenCleanupJob(a)
- go runCommandWebhookCleanupJob(a)
+ 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()
@@ -158,11 +175,12 @@ func runServer(configFileLocation string, disableConfigWatch bool) error {
a.Jobs.StartSchedulers()
}
+ notifyReady()
+
// wait for kill signal before attempting to gracefully shutdown
// the running service
- c := make(chan os.Signal, 1)
- signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
- <-c
+ signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+ <-interruptChan
if a.Cluster != nil {
a.Cluster.StopInterNodeCommunication()
@@ -229,6 +247,35 @@ func doDiagnostics(a *app.App) {
}
}
+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 != "" {
+ l4g.Info("Sending systemd READY notification.")
+
+ err := sendSystemdReadyNotification(systemdSocket)
+ if err != nil {
+ l4g.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()
}
diff --git a/cmd/platform/server_test.go b/cmd/platform/server_test.go
new file mode 100644
index 000000000..2f04e7d15
--- /dev/null
+++ b/cmd/platform/server_test.go
@@ -0,0 +1,136 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package main
+
+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/platform/test.go b/cmd/platform/test.go
index 036df07de..9ab3fbb36 100644
--- a/cmd/platform/test.go
+++ b/cmd/platform/test.go
@@ -53,7 +53,11 @@ func webClientTestsCmdF(cmd *cobra.Command, args []string) error {
defer a.Shutdown()
utils.InitTranslations(a.Config().LocalizationSettings)
- a.StartServer()
+ serverErr := a.StartServer()
+ if serverErr != nil {
+ return serverErr
+ }
+
api4.Init(a, a.Srv.Router, false)
api.Init(a, a.Srv.Router)
wsapi.Init(a, a.Srv.WebSocketRouter)
@@ -71,7 +75,11 @@ func serverForWebClientTestsCmdF(cmd *cobra.Command, args []string) error {
defer a.Shutdown()
utils.InitTranslations(a.Config().LocalizationSettings)
- a.StartServer()
+ serverErr := a.StartServer()
+ if serverErr != nil {
+ return serverErr
+ }
+
api4.Init(a, a.Srv.Router, false)
api.Init(a, a.Srv.Router)
wsapi.Init(a, a.Srv.WebSocketRouter)
diff --git a/cmd/platform/user.go b/cmd/platform/user.go
index 0913609f1..e2a8c9748 100644
--- a/cmd/platform/user.go
+++ b/cmd/platform/user.go
@@ -3,9 +3,12 @@
package main
import (
+ "encoding/json"
"errors"
"fmt"
+ "io/ioutil"
+ l4g "github.com/alecthomas/log4go"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/model"
"github.com/spf13/cobra"
@@ -87,25 +90,41 @@ var deleteAllUsersCmd = &cobra.Command{
}
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.
+ 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.")
+ }
-from_auth:
- The authentication service to migrate users accounts from.
- Supported options: email, gitlab, saml.
+ toAuth := args[1]
-to_auth:
- The authentication service to migrate users to.
- Supported options: ldap.
+ if toAuth != "ldap" && toAuth != "saml" {
+ return errors.New("Invalid to_auth parameter, must be saml or 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.
+ if toAuth == "ldap" && len(args) != 3 {
+ return errors.New("Ldap migration requires 3 arguments.")
+ }
-Will display any accounts that are not migrated successfully.`,
- Example: " user migrate_auth email ladp email",
- RunE: migrateAuthCmdF,
+ 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{
@@ -138,7 +157,69 @@ func init() {
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 occour even if there are duplicates on the LDAP server. Duplicates will not be migrated.")
+ 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,
@@ -416,27 +497,25 @@ func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error {
}
func migrateAuthCmdF(cmd *cobra.Command, args []string) error {
+ if args[1] == "saml" {
+ return migrateAuthToSamlCmdF(cmd, args)
+ }
+ return migrateAuthToLdapCmdF(cmd, args)
+}
+
+func migrateAuthToLdapCmdF(cmd *cobra.Command, args []string) error {
a, err := initDBCommandContextCobra(cmd)
if err != nil {
return err
}
- if len(args) != 3 {
- return errors.New("Expected three arguments. See help text for details.")
- }
-
fromAuth := args[0]
- toAuth := args[1]
- matchField := args[2]
+ matchField := args[1]
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 = ""
@@ -447,9 +526,10 @@ func migrateAuthCmdF(cmd *cobra.Command, args []string) error {
}
forceFlag, _ := cmd.Flags().GetBool("force")
+ dryRunFlag, _ := cmd.Flags().GetBool("dryRun")
if migrate := a.AccountMigration; migrate != nil {
- if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag); err != nil {
+ if err := migrate.MigrateToLdap(fromAuth, matchField, forceFlag, dryRunFlag); err != nil {
return errors.New("Error while migrating users: " + err.Error())
}
@@ -459,6 +539,61 @@ func migrateAuthCmdF(cmd *cobra.Command, args []string) error {
return nil
}
+func migrateAuthToSamlCmdF(cmd *cobra.Command, args []string) error {
+ a, err := initDBCommandContextCobra(cmd)
+ if err != nil {
+ return err
+ }
+
+ dryRunFlag, _ := cmd.Flags().GetBool("dryRun")
+ autoFlag, _ := cmd.Flags().GetBool("auto")
+
+ matchesFile := ""
+ matches := map[string]string{}
+ if !autoFlag {
+ matchesFile = args[1]
+
+ 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())
+ }
+ l4g.Close()
+ CommandPrettyPrintln("Sucessfully migrated accounts.")
+ }
+
+ return nil
+}
+
func verifyUserCmdF(cmd *cobra.Command, args []string) error {
a, err := initDBCommandContextCobra(cmd)
if err != nil {