diff options
author | Christopher Speller <crspeller@gmail.com> | 2018-07-10 01:54:25 -0700 |
---|---|---|
committer | Carlos Tadeu Panato Junior <ctadeu@gmail.com> | 2018-07-10 10:54:25 +0200 |
commit | 74e5d8ae66186a82e8afdd845a108d6a662751d7 (patch) | |
tree | faeeee70a251b636bae0afb4e60c611e00c2cc6b | |
parent | 951e4ad98401e9828b9941224318f105fb15d500 (diff) | |
download | chat-74e5d8ae66186a82e8afdd845a108d6a662751d7.tar.gz chat-74e5d8ae66186a82e8afdd845a108d6a662751d7.tar.bz2 chat-74e5d8ae66186a82e8afdd845a108d6a662751d7.zip |
MM-11120 Adding setting to disable email invitations and rate limiting. (#9063)
* Adding setting to disable email invitations.
* Adding a setting and rate limiting for email invite sending.
* Modifying email rate limit to 20/user/hour
* Adding EnableEmailInvitations to client side config and command.
-rw-r--r-- | api4/team_test.go | 9 | ||||
-rw-r--r-- | app/app.go | 8 | ||||
-rw-r--r-- | app/command_invite_people.go | 6 | ||||
-rw-r--r-- | app/email.go | 48 | ||||
-rw-r--r-- | app/team.go | 6 | ||||
-rw-r--r-- | cmd/mattermost/commands/user.go | 6 | ||||
-rw-r--r-- | config/default.json | 3 | ||||
-rw-r--r-- | i18n/en.json | 8 | ||||
-rw-r--r-- | model/config.go | 10 | ||||
-rw-r--r-- | utils/config.go | 2 |
10 files changed, 100 insertions, 6 deletions
diff --git a/api4/team_test.go b/api4/team_test.go index 48e3404eb..307e91635 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -1935,6 +1935,15 @@ func TestInviteUsersToTeam(t *testing.T) { utils.DeleteMailBox(user1) utils.DeleteMailBox(user2) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableEmailInvitations = false }) + + _, resp := th.SystemAdminClient.InviteUsersToTeam(th.BasicTeam.Id, emailList) + if resp.Error == nil { + t.Fatal("Should be disabled") + } + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableEmailInvitations = true }) + okMsg, resp := th.SystemAdminClient.InviteUsersToTeam(th.BasicTeam.Id, emailList) CheckNoError(t, resp) if !okMsg { diff --git a/app/app.go b/app/app.go index 96b9b6d13..6f98d4234 100644 --- a/app/app.go +++ b/app/app.go @@ -17,6 +17,7 @@ import ( "github.com/gorilla/mux" "github.com/pkg/errors" + "github.com/throttled/throttled" "github.com/mattermost/mattermost-server/einterfaces" ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs" @@ -46,7 +47,8 @@ type App struct { IsPluginSandboxSupported bool pluginStatuses map[string]*model.PluginStatus - EmailBatching *EmailBatchingJob + EmailBatching *EmailBatchingJob + EmailRateLimiter *throttled.GCRARateLimiter Hubs []*Hub HubsStopCheckingForDeadlock chan bool @@ -185,6 +187,10 @@ func New(options ...Option) (outApp *App, outErr error) { }) + if err := app.SetupInviteEmailRateLimiting(); err != nil { + return nil, err + } + mlog.Info("Server is initializing...") app.initEnterprise() diff --git a/app/command_invite_people.go b/app/command_invite_people.go index c3dc4f469..9ced3c5a1 100644 --- a/app/command_invite_people.go +++ b/app/command_invite_people.go @@ -28,7 +28,7 @@ func (me *InvitePeopleProvider) GetTrigger() string { func (me *InvitePeopleProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Command { autoComplete := true - if !a.Config().EmailSettings.SendEmailNotifications || !*a.Config().TeamSettings.EnableUserCreation { + if !a.Config().EmailSettings.SendEmailNotifications || !*a.Config().TeamSettings.EnableUserCreation || !*a.Config().ServiceSettings.EnableEmailInvitations { autoComplete = false } return &model.Command{ @@ -49,6 +49,10 @@ func (me *InvitePeopleProvider) DoCommand(a *App, args *model.CommandArgs, messa return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.invite_off")} } + if !*a.Config().ServiceSettings.EnableEmailInvitations { + return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.email_invitations_off")} + } + emailList := strings.Fields(message) for i := len(emailList) - 1; i >= 0; i-- { diff --git a/app/email.go b/app/email.go index b4e0a8983..569e6f454 100644 --- a/app/email.go +++ b/app/email.go @@ -10,12 +10,41 @@ import ( "net/http" "github.com/nicksnyder/go-i18n/i18n" + "github.com/pkg/errors" + "github.com/throttled/throttled" + "github.com/throttled/throttled/store/memstore" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" ) +const ( + emailRateLimitingMemstoreSize = 65536 + emailRateLimitingPerHour = 20 + emailRateLimitingMaxBurst = 20 +) + +func (a *App) SetupInviteEmailRateLimiting() error { + store, err := memstore.New(emailRateLimitingMemstoreSize) + if err != nil { + return errors.Wrap(err, "Unable to setup email rate limiting memstore.") + } + + quota := throttled.RateQuota{ + MaxRate: throttled.PerHour(emailRateLimitingPerHour), + MaxBurst: emailRateLimitingMaxBurst, + } + + rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) + if err != nil || rateLimiter == nil { + return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.") + } + + a.EmailRateLimiter = rateLimiter + return nil +} + func (a *App) SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL string) *model.AppError { T := utils.GetUserTranslations(locale) @@ -247,7 +276,24 @@ func (a *App) SendMfaChangeEmail(email string, activated bool, locale, siteURL s return nil } -func (a *App) SendInviteEmails(team *model.Team, senderName string, invites []string, siteURL string) { +func (a *App) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string) { + if a.EmailRateLimiter == nil { + a.Log.Error("Email invite not sent, rate limiting could not be setup.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id)) + return + } + rateLimited, result, err := a.EmailRateLimiter.RateLimit(senderUserId, len(invites)) + if rateLimited { + a.Log.Error("Invite emails rate limited.", + mlog.String("user_id", senderUserId), + mlog.String("team_id", team.Id), + mlog.String("retry_after", result.RetryAfter.String()), + mlog.Err(err)) + return + } else if err != nil { + a.Log.Error("Error rate limiting invite email.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id), mlog.Err(err)) + return + } + for _, invite := range invites { if len(invite) > 0 { senderRole := utils.T("api.team.invite_members.member") diff --git a/app/team.go b/app/team.go index beb4b1449..d9f19fab8 100644 --- a/app/team.go +++ b/app/team.go @@ -805,6 +805,10 @@ func (a *App) postRemoveFromTeamMessage(user *model.User, channel *model.Channel } func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string) *model.AppError { + if !*a.Config().ServiceSettings.EnableEmailInvitations { + return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented) + } + if len(emailList) == 0 { err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest) return err @@ -842,7 +846,7 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string) } nameFormat := *a.Config().TeamSettings.TeammateNameDisplay - a.SendInviteEmails(team, user.GetDisplayName(nameFormat), emailList, a.GetSiteURL()) + a.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL()) return nil } diff --git a/cmd/mattermost/commands/user.go b/cmd/mattermost/commands/user.go index 373fe7463..b3b43c076 100644 --- a/cmd/mattermost/commands/user.go +++ b/cmd/mattermost/commands/user.go @@ -384,7 +384,11 @@ func inviteUser(a *app.App, email string, team *model.Team, teamArg string) erro return fmt.Errorf("Can't find team '%v'", teamArg) } - a.SendInviteEmails(team, "Administrator", invites, *a.Config().ServiceSettings.SiteURL) + if !*a.Config().ServiceSettings.EnableEmailInvitations { + return fmt.Errorf("Email invites are disabled.") + } + + a.SendInviteEmails(team, "Administrator", "Mattermost CLI "+model.NewId(), invites, *a.Config().ServiceSettings.SiteURL) CommandPrettyPrintln("Invites may or may not have been sent.") return nil diff --git a/config/default.json b/config/default.json index a4487888e..cb60611ba 100644 --- a/config/default.json +++ b/config/default.json @@ -68,7 +68,8 @@ "ImageProxyURL": "", "EnableAPITeamDeletion": false, "ExperimentalEnableHardenedMode": false, - "ExperimentalLimitClientConfig": false + "ExperimentalLimitClientConfig": false, + "EnableEmailInvitations": false }, "TeamSettings": { "SiteName": "Mattermost", diff --git a/i18n/en.json b/i18n/en.json index b12a67cde..f257a86a4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -312,6 +312,10 @@ "translation": "Email has not been configured, no invite(s) sent" }, { + "id": "api.command.invite_people.email_invitations_off", + "translation": "Email invitations are disabled, no invite(s) sent" + }, + { "id": "api.command.invite_people.fail", "translation": "Encountered an error sending email invite(s)" }, @@ -1615,6 +1619,10 @@ "translation": "No one to invite." }, { + "id": "api.team.invite_members.disabled.app_error", + "translation": "Email invitations are disabled." + }, + { "id": "api.team.is_team_creation_allowed.disabled.app_error", "translation": "Team creation has been disabled. Please ask your systems administrator for details." }, diff --git a/model/config.go b/model/config.go index be940d893..1388f896f 100644 --- a/model/config.go +++ b/model/config.go @@ -237,9 +237,19 @@ type ServiceSettings struct { EnableAPITeamDeletion *bool ExperimentalEnableHardenedMode *bool ExperimentalLimitClientConfig *bool + EnableEmailInvitations *bool } func (s *ServiceSettings) SetDefaults() { + if s.EnableEmailInvitations == nil { + // If the site URL is also not present then assume this is a clean install + if s.SiteURL == nil { + s.EnableEmailInvitations = NewBool(false) + } else { + s.EnableEmailInvitations = NewBool(true) + } + } + if s.SiteURL == nil { s.SiteURL = NewString(SERVICE_SETTINGS_DEFAULT_SITE_URL) } diff --git a/utils/config.go b/utils/config.go index 10661aa54..f8cc6ec75 100644 --- a/utils/config.go +++ b/utils/config.go @@ -573,6 +573,8 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs) + props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations) + // Set default values for all options that require a license. props["ExperimentalHideTownSquareinLHS"] = "false" props["ExperimentalTownSquareIsReadOnly"] = "false" |