From f74b86ae9599ca2ade2e104f5ec88839537bff6b Mon Sep 17 00:00:00 2001 From: catalintomai <56169943+catalintomai@users.noreply.github.com> Date: Fri, 25 Sep 2020 14:59:41 -0700 Subject: [PATCH] MM-28733 : Admin Advisor v2 (#15515) --- api4/system.go | 33 +++- app/app.go | 219 +++++++++++++++++---- app/app_iface.go | 1 + app/integration_action.go | 95 +++++---- app/opentracing/opentracing_layer.go | 22 +++ app/server.go | 134 ++++++++++--- i18n/en.json | 196 ++++++++++++++++-- model/system.go | 70 +++++-- store/opentracinglayer/opentracinglayer.go | 36 ++++ store/retrylayer/retrylayer.go | 26 +++ store/sqlstore/system_store.go | 24 +++ store/sqlstore/user_store.go | 8 + store/store.go | 2 + store/storetest/mocks/SystemStore.go | 14 ++ store/storetest/mocks/UserStore.go | 23 +++ store/storetest/system_store.go | 28 +++ store/storetest/user_store.go | 39 ++++ store/timerlayer/timerlayer.go | 32 +++ 18 files changed, 878 insertions(+), 124 deletions(-) diff --git a/api4/system.go b/api4/system.go index 29976acbd4..ab83a260a8 100644 --- a/api4/system.go +++ b/api4/system.go @@ -62,7 +62,7 @@ func (api *API) InitSystem() { api.BaseRoutes.ApiRoot.Handle("/restart", api.ApiSessionRequired(restart)).Methods("POST") api.BaseRoutes.ApiRoot.Handle("/warn_metrics/status", api.ApiSessionRequired(getWarnMetricsStatus)).Methods("GET") api.BaseRoutes.ApiRoot.Handle("/warn_metrics/ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.ApiHandler(sendWarnMetricAckEmail)).Methods("POST") - + api.BaseRoutes.ApiRoot.Handle("/warn_metrics/trial-license-ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.ApiHandler(requestTrialLicenseAndAckWarnMetric)).Methods("POST") api.BaseRoutes.System.Handle("/notices/{team_id:[A-Za-z0-9]+}", api.ApiSessionRequired(getProductNotices)).Methods("GET") api.BaseRoutes.System.Handle("/notices/view", api.ApiSessionRequired(updateViewedProductNotices)).Methods("PUT") } @@ -731,6 +731,36 @@ func sendWarnMetricAckEmail(c *Context, w http.ResponseWriter, r *http.Request) ReturnStatusOK(w) } +func requestTrialLicenseAndAckWarnMetric(c *Context, w http.ResponseWriter, r *http.Request) { + auditRec := c.MakeAuditRecord("requestTrialLicenseAndAckWarnMetric", audit.Fail) + defer c.LogAuditRec(auditRec) + c.LogAudit("attempt") + + if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + if model.BuildEnterpriseReady != "true" { + mlog.Debug("Not Enterprise Edition, skip.") + return + } + + license := c.App.Srv().License() + if license != nil { + mlog.Debug("License is present, skip.") + return + } + + if err := c.App.RequestLicenseAndAckWarnMetric(c.Params.WarnMetricId, false); err != nil { + c.Err = err + return + } + + auditRec.Success() + ReturnStatusOK(w) +} + func getProductNotices(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireTeamId() if c.Err != nil { @@ -766,6 +796,7 @@ func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Reque c.Err = err return } + auditRec.Success() ReturnStatusOK(w) } diff --git a/app/app.go b/app/app.go index 7d35f2a63c..68643dce36 100644 --- a/app/app.go +++ b/app/app.go @@ -94,7 +94,7 @@ func (a *App) InitServer() { if a.Srv().runjobs { a.Srv().Go(func() { runLicenseExpirationCheckJob(a) - runCheckNumberOfActiveUsersWarnMetricStatusJob(a) + runCheckWarnMetricStatusJob(a) }) } a.srv.RunJobs() @@ -170,18 +170,32 @@ func (s *Server) getFirstServerRunTimestamp() (int64, *model.AppError) { return value, nil } +func (s *Server) getLastWarnMetricTimestamp() (int64, *model.AppError) { + systemData, err := s.Store.System().GetByName(model.SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY) + if err != nil { + return 0, model.NewAppError("getLastWarnMetricTimestamp", "app.system.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError) + } + value, err := strconv.ParseInt(systemData.Value, 10, 64) + if err != nil { + return 0, model.NewAppError("getLastWarnMetricTimestamp", "app.system_install_date.parse_int.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return value, nil +} + func (a *App) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) { systemDataList, nErr := a.Srv().Store.System().Get() if nErr != nil { return nil, model.NewAppError("GetWarnMetricsStatus", "app.system.get.app_error", nil, nErr.Error(), http.StatusInternalServerError) } + isE0Edition := model.BuildEnterpriseReady == "true" // license == nil was already validated upstream + result := map[string]*model.WarnMetricStatus{} for key, value := range systemDataList { if strings.HasPrefix(key, model.WARN_METRIC_STATUS_STORE_PREFIX) { if warnMetric, ok := model.WarnMetricsTable[key]; ok { - if !warnMetric.IsBotOnly && value == model.WARN_METRIC_STATUS_LIMIT_REACHED { - result[key], _ = a.getWarnMetricStatusAndDisplayTextsForId(key, nil) + if !warnMetric.IsBotOnly && (value == model.WARN_METRIC_STATUS_RUNONCE || value == model.WARN_METRIC_STATUS_LIMIT_REACHED) { + result[key], _ = a.getWarnMetricStatusAndDisplayTextsForId(key, nil, isE0Edition) } } } @@ -190,7 +204,7 @@ func (a *App) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model return result, nil } -func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18n.TranslateFunc) (*model.WarnMetricStatus, *model.WarnMetricDisplayTexts) { +func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18n.TranslateFunc, isE0Edition bool) (*model.WarnMetricStatus, *model.WarnMetricDisplayTexts) { var warnMetricStatus *model.WarnMetricStatus var warnMetricDisplayTexts = &model.WarnMetricDisplayTexts{} @@ -206,19 +220,90 @@ func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18 return warnMetricStatus, nil } - warnMetricDisplayTexts.BotMailToBody = T("api.server.warn_metric.bot_response.number_of_users.mailto_body", map[string]interface{}{"Limit": warnMetric.Limit}) - warnMetricDisplayTexts.EmailBody = T("api.templates.warn_metric_ack.number_of_active_users.body", map[string]interface{}{"Limit": warnMetric.Limit}) + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.bot_response.notification_success.message") switch warnMetricId { + case model.SYSTEM_WARN_METRIC_NUMBER_OF_TEAMS_5: + warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_teams_5.notification_title") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_teams_5.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_teams_5.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_teams_5.notification_body") + } + case model.SYSTEM_WARN_METRIC_MFA: + warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.mfa.notification_title") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.mfa.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.mfa.start_trial_notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.mfa.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.mfa.notification_body") + } + case model.SYSTEM_WARN_METRIC_EMAIL_DOMAIN: + warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.email_domain.notification_title") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.email_domain.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.email_domain.start_trial_notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.email_domain.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.email_domain.notification_body") + } + case model.SYSTEM_WARN_METRIC_NUMBER_OF_CHANNELS_50: + warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_channels_50.notification_title") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_channels_50.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_channels_50.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_channels_50.notification_body") + } + case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_100: + warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_100.notification_title") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_100.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_100.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_100.notification_body") + } case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200: warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_200.notification_title") - warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.notification_body") - case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400: - warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_400.notification_title") - warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_400.notification_body") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_200.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.notification_body") + } + case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300: + warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_title") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_300.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_300.notification_body") + } case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500: warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_500.notification_title") - warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.notification_body") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_active_users_500.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.notification_body") + } + case model.SYSTEM_WARN_METRIC_NUMBER_OF_POSTS_2M: + warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_posts_2M.notification_title") + if isE0Edition { + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_posts_2M.start_trial.notification_body") + warnMetricDisplayTexts.BotSuccessMessage = T("api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message") + } else { + warnMetricDisplayTexts.EmailBody = T("api.server.warn_metric.number_of_posts_2M.contact_us.email_body") + warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_posts_2M.notification_body") + } default: mlog.Error("Invalid metric id", mlog.String("id", warnMetricId)) return nil, nil @@ -229,7 +314,7 @@ func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18 return nil, nil } -func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppError { +func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string, isE0Edition bool) *model.AppError { perPage := 25 userOptions := &model.UserGetOptions{ Page: 0, @@ -281,7 +366,7 @@ func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppErro return appErr } - warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T) + warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, isE0Edition) if warnMetricStatus == nil { return model.NewAppError("NotifyAdminsOfWarnMetricStatus", "app.system.warn_metric.notification.invalid_metric.app_error", nil, "", http.StatusInternalServerError) } @@ -293,11 +378,23 @@ func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppErro Message: "", } + actionId := "contactUs" + actionName := T("api.server.warn_metric.contact_us") + postActionValue := T("api.server.warn_metric.contacting_us") + postActionUrl := fmt.Sprintf("/warn_metrics/ack/%s", warnMetricId) + + if isE0Edition { + actionId = "startTrial" + actionName = T("api.server.warn_metric.start_trial") + postActionValue = T("api.server.warn_metric.starting_trial") + postActionUrl = fmt.Sprintf("/warn_metrics/trial-license-ack/%s", warnMetricId) + } + actions := []*model.PostAction{} actions = append(actions, &model.PostAction{ - Id: "contactUs", - Name: T("api.server.warn_metric.contact_us"), + Id: actionId, + Name: actionName, Type: model.POST_ACTION_TYPE_BUTTON, Options: []*model.PostActionOptions{ { @@ -306,7 +403,7 @@ func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppErro }, { Text: "ActionExecutingMessage", - Value: T("api.server.warn_metric.contacting_us"), + Value: postActionValue, }, }, Integration: &model.PostActionIntegration{ @@ -314,7 +411,7 @@ func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppErro "bot_user_id": bot.UserId, "force_ack": false, }, - URL: fmt.Sprintf("/warn_metrics/ack/%s", warnMetricId), + URL: postActionUrl, }, }, ) @@ -327,7 +424,7 @@ func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppErro }} model.ParseSlackAttachment(botPost, attachments) - mlog.Debug("Send admin advisory for metric", mlog.String("warnMetricId", warnMetricId), mlog.String("userid", botPost.UserId)) + mlog.Debug("Post admin advisory for metric", mlog.String("warnMetricId", warnMetricId), mlog.String("userid", botPost.UserId)) if _, err := a.CreatePostAsUser(botPost, a.Session().Id, true); err != nil { return err } @@ -340,7 +437,7 @@ func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok { data, nErr := a.Srv().Store.System().GetByName(warnMetric.Id) if nErr == nil && data != nil && data.Value == model.WARN_METRIC_STATUS_ACK { - mlog.Debug("This metric warning has already been acknowledged") + mlog.Debug("This metric warning has already been acknowledged", mlog.String("id", warnMetric.Id)) return nil } @@ -369,7 +466,7 @@ func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, bodyPage.Props["TelemetryIdValue"] = a.TelemetryId() bodyPage.Props["Footer"] = T("api.templates.warn_metric_ack.footer") - warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T) + warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false) if warnMetricStatus == nil { return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", nil, "", http.StatusInternalServerError) } @@ -383,40 +480,96 @@ func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, } } - mlog.Debug("Disable the monitoring of all warn metrics") - err := a.setWarnMetricsStatus(model.WARN_METRIC_STATUS_ACK) - if err != nil { + if err := a.setWarnMetricsStatusAndNotify(warnMetric.Id); err != nil { return err } - - if !warnMetric.IsBotOnly && !isBot { - message := model.NewWebSocketEvent(model.WEBSOCKET_WARN_METRIC_STATUS_REMOVED, "", "", "", nil) - message.Add("warnMetricId", warnMetric.Id) - a.Publish(message) - } } return nil } +func (a *App) setWarnMetricsStatusAndNotify(warnMetricId string) *model.AppError { + // Ack all metric warnings on the server + if err := a.setWarnMetricsStatus(model.WARN_METRIC_STATUS_ACK); err != nil { + return err + } + + // Inform client that this metric warning has been acked + message := model.NewWebSocketEvent(model.WEBSOCKET_WARN_METRIC_STATUS_REMOVED, "", "", "", nil) + message.Add("warnMetricId", warnMetricId) + a.Publish(message) + + return nil +} + func (a *App) setWarnMetricsStatus(status string) *model.AppError { + mlog.Debug("Set monitoring status for all warn metrics", mlog.String("status", status)) for _, warnMetric := range model.WarnMetricsTable { - a.setWarnMetricsStatusForId(warnMetric.Id, status) + if err := a.setWarnMetricsStatusForId(warnMetric.Id, status); err != nil { + return err + } } return nil } func (a *App) setWarnMetricsStatusForId(warnMetricId string, status string) *model.AppError { - mlog.Info("Storing user acknowledgement for warn metric", mlog.String("warnMetricId", warnMetricId)) - if err := a.Srv().Store.System().SaveOrUpdate(&model.System{ + mlog.Debug("Store status for warn metric", mlog.String("warnMetricId", warnMetricId), mlog.String("status", status)) + if err := a.Srv().Store.System().SaveOrUpdateWithWarnMetricHandling(&model.System{ Name: warnMetricId, Value: status, }); err != nil { mlog.Error("Unable to write to database.", mlog.Err(err)) - return model.NewAppError("setWarnMetricsStatusForId", "app.system.warn_metric.store.app_error", map[string]interface{}{"WarnMetricName": warnMetricId}, "", http.StatusInternalServerError) + return model.NewAppError("setWarnMetricsStatusForId", "app.system.warn_metric.store.app_error", map[string]interface{}{"WarnMetricName": warnMetricId}, err.Error(), http.StatusInternalServerError) } return nil } +func (a *App) RequestLicenseAndAckWarnMetric(warnMetricId string, isBot bool) *model.AppError { + if *a.Config().ExperimentalSettings.RestrictSystemAdmin { + return model.NewAppError("RequestLicenseAndAckWarnMetric", "api.restricted_system_admin", nil, "", http.StatusForbidden) + } + + currentUser, appErr := a.GetUser(a.Session().UserId) + if appErr != nil { + return appErr + } + + registeredUsersCount, err := a.Srv().Store.User().Count(model.UserCountOptions{}) + if err != nil { + mlog.Error("Error retrieving the number of registered users", mlog.Err(err)) + return model.NewAppError("RequestLicenseAndAckWarnMetric", "api.license.request_trial_license.fail_get_user_count.app_error", nil, err.Error(), http.StatusBadRequest) + } + + trialLicenseRequest := &model.TrialLicenseRequest{ + ServerID: a.TelemetryId(), + Name: currentUser.GetDisplayName(model.SHOW_FULLNAME), + Email: currentUser.Email, + SiteName: *a.Config().TeamSettings.SiteName, + SiteURL: *a.Config().ServiceSettings.SiteURL, + Users: int(registeredUsersCount), + TermsAccepted: true, + ReceiveEmailsAccepted: true, + } + + if trialLicenseRequest.SiteURL == "" { + return model.NewAppError("RequestLicenseAndAckWarnMetric", "api.license.request_trial_license.no-site-url.app_error", nil, "", http.StatusBadRequest) + } + + if err := a.Srv().RequestTrialLicense(trialLicenseRequest); err != nil { + // turn off warn metric warning even in case of StartTrial failure + if nerr := a.setWarnMetricsStatusAndNotify(warnMetricId); nerr != nil { + return nerr + } + + return err + } + + if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, currentUser, true, isBot); appErr != nil { + return appErr + } + + return nil +} + func (a *App) Srv() *Server { return a.srv } diff --git a/app/app_iface.go b/app/app_iface.go index 1ee33cf774..c825f3f8a5 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -829,6 +829,7 @@ type AppIface interface { RemoveUserFromTeam(teamId string, userId string, requestorId string) *model.AppError RemoveUsersFromChannelNotMemberOfTeam(remover *model.User, channel *model.Channel, team *model.Team) *model.AppError RequestId() string + RequestLicenseAndAckWarnMetric(warnMetricId string, isBot bool) *model.AppError ResetPasswordFromToken(userSuppliedTokenString, newPassword string) *model.AppError ResetPermissionsSystem() *model.AppError RestoreChannel(channel *model.Channel, userId string) (*model.Channel, *model.AppError) diff --git a/app/integration_action.go b/app/integration_action.go index ef27b66b2b..b56adeb612 100644 --- a/app/integration_action.go +++ b/app/integration_action.go @@ -432,6 +432,12 @@ func (a *App) doLocalWarnMetricsRequest(rawURL string, upstreamRequest *model.Po return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest) } + license := a.Srv().License() + if license != nil { + mlog.Debug("License is present, skip this call") + return nil + } + user, appErr := a.GetUser(a.Session().UserId) if appErr != nil { return appErr @@ -443,48 +449,55 @@ func (a *App) doLocalWarnMetricsRequest(rawURL string, upstreamRequest *model.Po HasReactions: true, } - forceAck := upstreamRequest.Context["force_ack"].(bool) + isE0Edition := (model.BuildEnterpriseReady == "true") // license == nil was already validated upstream + _, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, utils.T, isE0Edition) + botPost.Message = ":white_check_mark: " + warnMetricDisplayTexts.BotSuccessMessage - if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, user, forceAck, true); appErr != nil { - if forceAck { - return appErr + if isE0Edition { + if appErr = a.RequestLicenseAndAckWarnMetric(warnMetricId, true); appErr != nil { + botPost.Message = ":warning: " + utils.T("api.server.warn_metric.bot_response.start_trial_failure.message") } - mailtoLinkText := a.buildWarnMetricMailtoLink(warnMetricId, user) - botPost.Message = ":warning: " + utils.T("api.server.warn_metric.bot_response.notification_failure.message") - actions := []*model.PostAction{} - actions = append(actions, - &model.PostAction{ - Id: "emailUs", - Name: utils.T("api.server.warn_metric.email_us"), - Type: model.POST_ACTION_TYPE_BUTTON, - Options: []*model.PostActionOptions{ - { - Text: "WarnMetricMailtoUrl", - Value: mailtoLinkText, - }, - { - Text: "TrackEventId", - Value: warnMetricId, - }, - }, - Integration: &model.PostActionIntegration{ - Context: model.StringInterface{ - "bot_user_id": botPost.UserId, - "force_ack": true, - }, - URL: fmt.Sprintf("/warn_metrics/ack/%s", model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500), - }, - }, - ) - attachements := []*model.SlackAttachment{{ - AuthorName: "", - Title: "", - Actions: actions, - Text: utils.T("api.server.warn_metric.bot_response.notification_failure.body"), - }} - model.ParseSlackAttachment(botPost, attachements) } else { - botPost.Message = ":white_check_mark: " + utils.T("api.server.warn_metric.bot_response.notification_success.message") + forceAck := upstreamRequest.Context["force_ack"].(bool) + if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, user, forceAck, true); appErr != nil { + if forceAck { + return appErr + } + mailtoLinkText := a.buildWarnMetricMailtoLink(warnMetricId, user) + botPost.Message = ":warning: " + utils.T("api.server.warn_metric.bot_response.notification_failure.message") + actions := []*model.PostAction{} + actions = append(actions, + &model.PostAction{ + Id: "emailUs", + Name: utils.T("api.server.warn_metric.email_us"), + Type: model.POST_ACTION_TYPE_BUTTON, + Options: []*model.PostActionOptions{ + { + Text: "WarnMetricMailtoUrl", + Value: mailtoLinkText, + }, + { + Text: "TrackEventId", + Value: warnMetricId, + }, + }, + Integration: &model.PostActionIntegration{ + Context: model.StringInterface{ + "bot_user_id": botPost.UserId, + "force_ack": true, + }, + URL: fmt.Sprintf("/warn_metrics/ack/%s", model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500), + }, + }, + ) + attachements := []*model.SlackAttachment{{ + AuthorName: "", + Title: "", + Actions: actions, + Text: utils.T("api.server.warn_metric.bot_response.notification_failure.body"), + }} + model.ParseSlackAttachment(botPost, attachements) + } } if _, err := a.CreatePostAsUser(botPost, a.Session().Id, true); err != nil { @@ -509,9 +522,9 @@ func (mlc *MailToLinkContent) ToJson() string { func (a *App) buildWarnMetricMailtoLink(warnMetricId string, user *model.User) string { T := utils.GetUserTranslations(user.Locale) - _, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T) + _, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false) - mailBody := warnMetricDisplayTexts.BotMailToBody + mailBody := warnMetricDisplayTexts.EmailBody mailBody += T("api.server.warn_metric.bot_response.mailto_contact_header", map[string]interface{}{"Contact": user.GetFullName()}) mailBody += "\r\n" mailBody += T("api.server.warn_metric.bot_response.mailto_email_header", map[string]interface{}{"Email": user.Email}) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 2052cf4b7a..a455f7c9aa 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -11795,6 +11795,28 @@ func (a *OpenTracingAppLayer) RenameTeam(team *model.Team, newTeamName string, n return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) RequestLicenseAndAckWarnMetric(warnMetricId string, isBot bool) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RequestLicenseAndAckWarnMetric") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.RequestLicenseAndAckWarnMetric(warnMetricId, isBot) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) ResetPasswordFromToken(userSuppliedTokenString string, newPassword string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResetPasswordFromToken") diff --git a/app/server.go b/app/server.go index 09ecc37abf..b472b92bcd 100644 --- a/app/server.go +++ b/app/server.go @@ -1166,11 +1166,11 @@ func runLicenseExpirationCheckJob(a *App) { }, time.Hour*24) } -func runCheckNumberOfActiveUsersWarnMetricStatusJob(a *App) { - doCheckNumberOfActiveUsersWarnMetricStatus(a) - model.CreateRecurringTask("Check Number Of Active Users Warn Metric Status", func() { - doCheckNumberOfActiveUsersWarnMetricStatus(a) - }, time.Hour*24*7) +func runCheckWarnMetricStatusJob(a *App) { + doCheckWarnMetricStatus(a) + model.CreateRecurringTask("Check Warn Metric Status Job", func() { + doCheckWarnMetricStatus(a) + }, time.Hour*model.WARN_METRIC_JOB_INTERVAL) } func doSecurity(s *Server) { @@ -1193,51 +1193,139 @@ func doSessionCleanup(s *Server) { s.Store.Session().Cleanup(model.GetMillis(), SESSIONS_CLEANUP_BATCH_SIZE) } -func doCheckNumberOfActiveUsersWarnMetricStatus(a *App) { +func doCheckWarnMetricStatus(a *App) { license := a.Srv().License() if license != nil { - mlog.Debug("License is present, skip this check") + mlog.Debug("License is present, skip") return } - numberOfActiveUsers, err := a.Srv().Store.User().Count(model.UserCountOptions{}) + // Get the system fields values from store + systemDataList, nErr := a.Srv().Store.System().Get() + if nErr != nil { + mlog.Error("No system properties obtained", mlog.Err(nErr)) + return + } + + warnMetricStatusFromStore := make(map[string]string) + + for key, value := range systemDataList { + if strings.HasPrefix(key, model.WARN_METRIC_STATUS_STORE_PREFIX) { + if _, ok := model.WarnMetricsTable[key]; ok { + warnMetricStatusFromStore[key] = value + if value == model.WARN_METRIC_STATUS_ACK { + // If any warn metric has already been acked, we return + mlog.Debug("Warn metrics have been acked, skip") + return + } + } + } + } + + lastWarnMetricRunTimestamp, err := a.Srv().getLastWarnMetricTimestamp() if err != nil { - mlog.Error("Error to get active registered users.", mlog.Err(err)) + mlog.Debug("Cannot obtain last advisory run timestamp", mlog.Err(err)) + } else { + currentTime := utils.MillisFromTime(time.Now()) + // If the admin advisory has already been shown in the last 7 days + if (currentTime-lastWarnMetricRunTimestamp)/(model.WARN_METRIC_JOB_WAIT_TIME) < 1 { + mlog.Debug("No advisories should be shown during the wait interval time") + return + } + } + + numberOfActiveUsers, err0 := a.Srv().Store.User().Count(model.UserCountOptions{}) + if err0 != nil { + mlog.Error("Error attempting to get active registered users.", mlog.Err(err0)) + } + + teamCount, err1 := a.Srv().Store.Team().AnalyticsTeamCount(false) + if err1 != nil { + mlog.Error("Error attempting to get number of teams.", mlog.Err(err1)) + } + + openChannelCount, err2 := a.Srv().Store.Channel().AnalyticsTypeCount("", model.CHANNEL_OPEN) + if err2 != nil { + mlog.Error("Error attempting to get number of public channels.", mlog.Err(err2)) + } + + // If an account is created with a different email domain + // Search for an entry that has an email account different from the current domain + // Get domain account from site url + localDomainAccount := utils.GetHostnameFromSiteURL(*a.Srv().Config().ServiceSettings.SiteURL) + isDiffEmailAccount, err3 := a.Srv().Store.User().AnalyticsGetExternalUsers(localDomainAccount) + if err3 != nil { + mlog.Error("Error attempting to get number of private channels.", mlog.Err(err3)) } warnMetrics := []model.WarnMetric{} - if numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit { + + if numberOfActiveUsers < model.WARN_METRIC_NUMBER_OF_ACTIVE_USERS_25 { return - } else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400].Limit { - warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200]) - } else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500].Limit { - warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400]) - } else { - warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500]) + } else if teamCount >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_TEAMS_5].Limit && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_NUMBER_OF_TEAMS_5] != model.WARN_METRIC_STATUS_RUNONCE { + warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_TEAMS_5]) + } else if *a.Config().ServiceSettings.EnableMultifactorAuthentication && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_MFA] != model.WARN_METRIC_STATUS_RUNONCE { + warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_MFA]) + } else if isDiffEmailAccount && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_EMAIL_DOMAIN] != model.WARN_METRIC_STATUS_RUNONCE { + warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_EMAIL_DOMAIN]) + } else if openChannelCount >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_CHANNELS_50].Limit && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_NUMBER_OF_CHANNELS_50] != model.WARN_METRIC_STATUS_RUNONCE { + warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_CHANNELS_50]) } + // If the system did not cross any of the thresholds for the Contextual Advisories + if len(warnMetrics) == 0 { + if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_100].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_100] != model.WARN_METRIC_STATUS_RUNONCE { + warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_100]) + } else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300].Limit && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200] != model.WARN_METRIC_STATUS_RUNONCE { + warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200]) + } else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500].Limit && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300] != model.WARN_METRIC_STATUS_RUNONCE { + warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300]) + } else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500].Limit { + var tWarnMetric model.WarnMetric + + if warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500] != model.WARN_METRIC_STATUS_RUNONCE { + tWarnMetric = model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500] + } + + postsCount, err4 := a.Srv().Store.Post().AnalyticsPostCount("", false, false) + if err4 != nil { + mlog.Error("Error attempting to get number of posts.", mlog.Err(err4)) + } + + if postsCount > model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_POSTS_2M].Limit && warnMetricStatusFromStore[model.SYSTEM_WARN_METRIC_NUMBER_OF_POSTS_2M] != model.WARN_METRIC_STATUS_RUNONCE { + tWarnMetric = model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_POSTS_2M] + } + + if tWarnMetric != (model.WarnMetric{}) { + warnMetrics = append(warnMetrics, tWarnMetric) + } + } + } + + isE0Edition := model.BuildEnterpriseReady == "true" // license == nil was already validated upstream + for _, warnMetric := range warnMetrics { data, nErr := a.Srv().Store.System().GetByName(warnMetric.Id) - if nErr == nil && data != nil && (data.Value == model.WARN_METRIC_STATUS_ACK || (warnMetric.IsBotOnly && data.Value == model.WARN_METRIC_STATUS_RUNONCE)) { - mlog.Debug("This metric warning has already been acked or it is bot only and ran once") + if nErr == nil && data != nil && warnMetric.IsBotOnly && data.Value == model.WARN_METRIC_STATUS_RUNONCE { + mlog.Debug("This metric warning is bot only and ran once") continue } - warnMetricStatus, _ := a.getWarnMetricStatusAndDisplayTextsForId(warnMetric.Id, nil) + warnMetricStatus, _ := a.getWarnMetricStatusAndDisplayTextsForId(warnMetric.Id, nil, isE0Edition) if !warnMetric.IsBotOnly { - // Banner and bot metrics - send websocket event + // Banner and bot metric types - send websocket event every interval message := model.NewWebSocketEvent(model.WEBSOCKET_WARN_METRIC_STATUS_RECEIVED, "", "", "", nil) message.Add("warnMetricStatus", warnMetricStatus.ToJson()) a.Publish(message) - // Bot and banner metrics, do not send the bot message again + // Banner and bot metric types, send the bot message only once if data != nil && data.Value == model.WARN_METRIC_STATUS_RUNONCE { continue } } - if err = a.notifyAdminsOfWarnMetricStatus(warnMetric.Id); err != nil { - mlog.Error("Failed to send notifications to admin users.", mlog.Err(err)) + if nerr := a.notifyAdminsOfWarnMetricStatus(warnMetric.Id, isE0Edition); nerr != nil { + mlog.Error("Failed to send notifications to admin users.", mlog.Err(nerr)) } if warnMetric.IsRunOnce { diff --git a/i18n/en.json b/i18n/en.json index 31cd77d09a..f5e9ce52b5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1564,6 +1564,10 @@ "id": "api.license.request_trial_license.app_error", "translation": "Unable to get a trial license, please try again or contact with support@mattermost.com." }, + { + "id": "api.license.request_trial_license.fail_get_user_count.app_error", + "translation": "Unable to get a trial license, please try again or contact with support@mattermost.com. Cannot obtain the number of registered users." + }, { "id": "api.license.request_trial_license.no-site-url.app_error", "translation": "Unable to request a trial license. Please configure a Site URL in the web server section of the Mattermost System Console." @@ -2023,45 +2027,209 @@ "translation": "Thank you for contacting Mattermost. We will follow up with you soon." }, { - "id": "api.server.warn_metric.bot_response.number_of_users.mailto_body", - "translation": "Mattermost Contact Us request. My team has now {{.Limit}} users and I am considering Mattermost Enterprise Edition.\r\n" + "id": "api.server.warn_metric.bot_response.start_trial_failure.message", + "translation": "Trial license could not be retrieved. Visit https://mattermost.com/trial/ to request a license." }, { "id": "api.server.warn_metric.contact_us", - "translation": "Acknowledge" + "translation": "Contact Us" }, { "id": "api.server.warn_metric.contacting_us", - "translation": "Acknowledging" + "translation": "Contacting Us" + }, + { + "id": "api.server.warn_metric.email_domain.contact_us.email_body", + "translation": "Mattermost contact us request. I'm interested in learning more about using Guest Accounts.\r\n" + }, + { + "id": "api.server.warn_metric.email_domain.notification_body", + "translation": "Projects often involve people both inside and outside of an organization. With Guest Accounts, you can bring external partners into your Mattermost system and specify who they can work with and what they can see.\r\n\r\n[Learn more about enabling Guest Accounts](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts).\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.email_domain.notification_title", + "translation": "Creating Guest Accounts" + }, + { + "id": "api.server.warn_metric.email_domain.start_trial.notification_body", + "translation": "Projects often involve people both inside and outside of an organization. With Guest Accounts, you can bring external partners into your Mattermost system and specify who they can work with and what they can see.\r\n\r\n[Learn more about enabling Guest Accounts](https://www.mattermost.com/docs-guest-accounts/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=guest-accounts)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.email_domain.start_trial_notification_success.message", + "translation": "Your Enterprise trial is now active. Go to **System Console > Authentication > Guest Access** to enable Guest Accounts." }, { "id": "api.server.warn_metric.email_us", "translation": "Email us" }, + { + "id": "api.server.warn_metric.mfa.contact_us.email_body", + "translation": "Mattermost contact us request. I'm interested in learning more about enforcing Multi-Factor Authentication.\r\n" + }, + { + "id": "api.server.warn_metric.mfa.notification_body", + "translation": "Your Mattermost system has multi-factor authentication enabled, giving users the choice to secure their accounts with additional means of authentication beyond a password. To improve security across the system you can require all Mattermost accounts to use multi-factor authentication.\r\n\r\n[Learn more about enforcing Multi-Factor Authentication](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication). \r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.mfa.notification_title", + "translation": "Enforcing Multi-Factor Authentication" + }, + { + "id": "api.server.warn_metric.mfa.start_trial.notification_body", + "translation": "Your Mattermost system has multi-factor authentication enabled, giving users the choice to secure their accounts with additional means of authentication beyond a password. To improve security across the system you can require all Mattermost accounts to use multi-factor authentication.\r\n\r\n[Learn more about enforcing Multi-Factor Authentication](https://www.mattermost.com/docs-multi-factor-authentication/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=multi-factor-authentication)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.mfa.start_trial_notification_success.message", + "translation": "Your Enterprise trial is now active. Go to **System Console > Authentication > MFA** to enforce multi-factor authentication." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.contact_us.email_body", + "translation": "Mattermost contact us request. My team now has 100 users, and I'm considering Mattermost Enterprise Edition.\r\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_body", + "translation": "Your Mattermost system has over 100 users. As your user base grows, provisioning new accounts can become time-consuming. We recommend that you integrate your organization’s Active Directory/LDAP, which will allow anyone with an account to access Mattermost.\r\n\r\n[Learn more about integrating with AD/LDAP](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.notification_title", + "translation": "Scaling with Mattermost" + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_body", + "translation": "Your Mattermost system has over 100 users. As your user base grows, provisioning new accounts can become time-consuming. We recommend that you integrate your organization’s Active Directory/LDAP, which will allow anyone with an account to access Mattermost.\r\n\r\n[Learn more about integrating with AD/LDAP](https://www.mattermost.com/docs-adldap/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.number_of_active_users_100.start_trial.notification_success.message", + "translation": "Your Enterprise trial is now active. Go to **System Console > Authentication > AD/LDAP** to integrate your AD/LDAP service." + }, + { + "id": "api.server.warn_metric.number_of_active_users_200.contact_us.email_body", + "translation": "Mattermost contact us request. My team now has 200 users, and I'm considering Mattermost Enterprise Edition.\r\n" + }, { "id": "api.server.warn_metric.number_of_active_users_200.notification_body", - "translation": "Your Mattermost system now has 200 users. As your user base grows, provisioning new accounts can become time-consuming. We recommend that you integrate your organization’s Active Directory/LDAP, which will allow anyone with an account to access Mattermost.\r\n\r\n[Learn more about integrating with AD/LDAP](https://docs.mattermost.com/deployment/sso-ldap.html?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)\r\n\r\nBy clicking Acknowledge, you'll be sharing your information with Mattermost Inc., to learn more about upgrading. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + "translation": "Your Mattermost system now has 200 users. When you connect Mattermost with your organization's single sign-on provider, users can access Mattermost without having to re-enter their credentials. We recommend you integrate your SAML 2.0 provider with your Mattermost server.[Learn more about integrating with SAML 2.0](https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml).\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" }, { "id": "api.server.warn_metric.number_of_active_users_200.notification_title", "translation": "Scaling with Mattermost" }, { - "id": "api.server.warn_metric.number_of_active_users_400.notification_body", - "translation": "Your Mattermost system now has 400 users. When you connect Mattermost with your organization's single sign-on provider, users can access Mattermost without having to re-enter their credentials. We recommend you integrate SAML 2.0 provider with your Mattermost server.\r\n\r\n[Learn more about integrating with SAML 2.0](https://docs.mattermost.com/deployment/sso-saml.html?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml)\r\n\r\nBy clicking Acknowledge, you'll be sharing your information with Mattermost Inc., to learn more about upgrading. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_body", + "translation": "Your Mattermost system now has 200 users. When you connect Mattermost with your organization's single sign-on provider, users can access Mattermost without having to re-enter their credentials. We recommend you integrate your SAML 2.0 provider with your Mattermost server.[Learn more about integrating with SAML 2.0](https://www.mattermost.com/docs-saml/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." }, { - "id": "api.server.warn_metric.number_of_active_users_400.notification_title", - "translation": "Scaling with Mattermost" + "id": "api.server.warn_metric.number_of_active_users_200.start_trial.notification_success.message", + "translation": "Your Enterprise trial is now active. Go to **System Console > Authentication > SAML 2.0** to integrate with your SAML 2.0 provider." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.contact_us.email_body", + "translation": "Mattermost contact us request. I'm interested in learning more about creating read-only Announcement Channels.\r\n" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.notification_body", + "translation": "With so much conversation happening across Mattermost, it can be challenging to know where to look for important information. If you want to broadcast a message to a large audience, you can set up read-only Announcement Channels where anyone can join but only channel admins can post messages.\r\n\r\n[Learn more about creating read-only Announcement Channels](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_body", + "translation": "With so much conversation happening across Mattermost, it can be challenging to know where to look for important information. If you want to broadcast a message to a large audience, you can set up read-only Announcement Channels where anyone can join but only channel admins can post messages.\r\n\r\n[Learn more about creating read-only Announcement Channels](https://www.mattermost.com/docs-channel-moderation/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=channel-moderation)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_success.message", + "translation": "Your Enterprise trial is now active. Create a channel and go to **System Console > User Management > Channels** to limit posting to channel admins." + }, + { + "id": "api.server.warn_metric.number_of_active_users_300.start_trial.notification_title", + "translation": "Read-Only Announcement Channels" + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.contact_us.email_body", + "translation": "Mattermost contact us request. My team now has 500 users, and I'm considering Mattermost Enterprise Edition.\r\n" }, { "id": "api.server.warn_metric.number_of_active_users_500.notification_body", - "translation": "Mattermost strongly recommends that deployments of over 500 users take advantage of features such as user management, server clustering and performance monitoring. Contact us to learn more and let us know how we can help.\r\n\r\nBy clicking Acknowledge, you'll be sharing your information with Mattermost Inc., to learn more about upgrading. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + "translation": "Mattermost strongly recommends that deployments of over 500 users take advantage of features such as user management, server clustering and performance monitoring. Contact us to learn more and let us know how we can help.\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" }, { "id": "api.server.warn_metric.number_of_active_users_500.notification_title", "translation": "Scaling with Mattermost" }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_body", + "translation": "Mattermost strongly recommends that deployments of over 500 users take advantage of features such as user management, server clustering and performance monitoring. Contact us to learn more and let us know how we can help.\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.number_of_active_users_500.start_trial.notification_success.message", + "translation": "Your Enterprise trial is now active. Go to the System Console to enable advanced features." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.contact_us.email_body", + "translation": "Mattermost contact us request. I'm interested in learning more about using Advanced Permissions with System Schemes.\r\n" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_body", + "translation": "Channels help improve communication, but with users across Mattermost joining and creating channels, the challenge of keeping the system organized increases. Advanced Permissions enable you to set which users or roles can perform certain actions, including managing channel settings and members, using @channel or @here to tag broad groups of users, and creating new webhooks.\r\n\r\n[Learn more about using Advanced Permissions](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.notification_title", + "translation": "Using Advanced Permissions" + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_body", + "translation": "Channels help improve communication, but with users across Mattermost joining and creating channels, the challenge of keeping the system organized increases. Advanced Permissions enable you to set which users or roles can perform certain actions, including managing channel settings and members, using @channel or @here to tag broad groups of users, and creating new webhooks.\r\n\r\n[Learn more about using Advanced Permissions](https://www.mattermost.com/docs-advanced-permissions/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.number_of_channels_50.start_trial.notification_success.message", + "translation": "Your Enterprise trial is now active. Go to **System Console > User Management > Permissions** to enable Advanced Permissions." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.contact_us.email_body", + "translation": "Mattermost contact us request. I'm interested in learning more about improving performance with Elasticsearch.\r\n" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_body", + "translation": "Your Mattermost system has a large number of messages. The default Mattermost database search starts to show performance degradation at around 2.5 million posts. With over 5 million posts, Elasticsearch can help avoid significant performance issues, such as timeouts, with search and at-mentions. Contact us to learn more and let us know how we can help.\r\n\r\n[Learn more about improving performance](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_title", + "translation": "Improving Performance" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_body", + "translation": "Your Mattermost system has a large number of messages. The default Mattermost database search starts to show performance degradation at around 2.5 million posts. With over 5 million posts, Elasticsearch can help avoid significant performance issues, such as timeouts, with search and at-mentions. Contact us to learn more and let us know how we can help.\r\n\r\n[Learn more about improving performance](https://www.mattermost.com/docs-elasticsearch/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=elasticsearch)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.start_trial.notification_success.message", + "translation": "Your Enterprise trial is now active. Once you have an Elasticsearch server, go to **System Console > Environment > Elasticsearch** to configure Elasticsearch." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.contact_us.email_body", + "translation": "Mattermost contact us request. I'm interested in learning more about Advanced Permissions with Team Schemes.\r\n" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_body", + "translation": "Your Mattermost system now has several teams. Many teams have their own preferred way of coordinating and collaborating, including how channels are created, who can invite new teammates, and how integrations are managed. Team Override Schemes allow you to customize user permissions within each team to meet their specific needs.\r\n\r\n[Learn more about using Advanced Permissions](https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override).\r\n\r\nBy clicking Contact Us, you'll be sharing your information with Mattermost, Inc. [Learn more](https://mattermost.com/pl/default-admin-advisory)" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_title", + "translation": "Using Advanced Permissions" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial.notification_body", + "translation": "Your Mattermost system now has several teams. Many teams have their own preferred way of coordinating and collaborating, including how channels are created, who can invite new teammates, and how integrations are managed. Team Override Schemes allow you to customize user permissions within each team to meet their specific needs.\r\n\r\n[Learn more about using Advanced Permissions](https://www.mattermost.com/docs-advanced-permissions-team-override/?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=advanced-permissions-team-override)\r\n\r\nBy clicking Start trial, I agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message", + "translation": "Your Enterprise trial is now active. Go to **System Console > User Management > Permissions** to enable Advanced Permissions." + }, + { + "id": "api.server.warn_metric.start_trial", + "translation": "Start Trial" + }, + { + "id": "api.server.warn_metric.starting_trial", + "translation": "Getting Trial" + }, { "id": "api.slackimport.slack_add_bot_user.email_pwd", "translation": "The Integration/Slack Bot user with email {{.Email}} and password {{.Password}} has been imported.\r\n" @@ -2650,10 +2818,6 @@ "id": "api.templates.warn_metric_ack.footer", "translation": "If you have any additional inquiries, please contact support@mattermost.com" }, - { - "id": "api.templates.warn_metric_ack.number_of_active_users.body", - "translation": "Mattermost Contact Us request. My team has now {{ .Limit }} users and I am considering Mattermost Enterprise Edition." - }, { "id": "api.templates.warn_metric_ack.subject", "translation": "Mattermost Contact Us request" @@ -7730,6 +7894,10 @@ "id": "store.sql_user.analytics_daily_active_users.app_error", "translation": "Unable to get the active users during the requested period." }, + { + "id": "store.sql_user.analytics_get_external_users.app_error", + "translation": "We could not count the users with non local domain emails" + }, { "id": "store.sql_user.analytics_get_inactive_users_count.app_error", "translation": "We could not count the inactive users." diff --git a/model/system.go b/model/system.go index 11730fb43f..f826276f06 100644 --- a/model/system.go +++ b/model/system.go @@ -21,16 +21,26 @@ const ( SYSTEM_FIRST_SERVER_RUN_TIMESTAMP_KEY = "FirstServerRunTimestamp" SYSTEM_CLUSTER_ENCRYPTION_KEY = "ClusterEncryptionKey" SYSTEM_UPGRADED_FROM_TE_ID = "UpgradedFromTE" + SYSTEM_WARN_METRIC_NUMBER_OF_TEAMS_5 = "warn_metric_number_of_teams_5" + SYSTEM_WARN_METRIC_NUMBER_OF_CHANNELS_50 = "warn_metric_number_of_channels_50" + SYSTEM_WARN_METRIC_MFA = "warn_metric_mfa" + SYSTEM_WARN_METRIC_EMAIL_DOMAIN = "warn_metric_email_domain" + SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_100 = "warn_metric_number_of_active_users_100" SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200 = "warn_metric_number_of_active_users_200" - SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400 = "warn_metric_number_of_active_users_400" + SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300 = "warn_metric_number_of_active_users_300" SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500 = "warn_metric_number_of_active_users_500" + SYSTEM_WARN_METRIC_NUMBER_OF_POSTS_2M = "warn_metric_number_of_posts_2M" + SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY = "LastWarnMetricRunTimestamp" ) const ( - WARN_METRIC_STATUS_LIMIT_REACHED = "true" - WARN_METRIC_STATUS_RUNONCE = "runonce" - WARN_METRIC_STATUS_ACK = "ack" - WARN_METRIC_STATUS_STORE_PREFIX = "warn_metric_" + WARN_METRIC_STATUS_LIMIT_REACHED = "true" + WARN_METRIC_STATUS_RUNONCE = "runonce" + WARN_METRIC_STATUS_ACK = "ack" + WARN_METRIC_STATUS_STORE_PREFIX = "warn_metric_" + WARN_METRIC_JOB_INTERVAL = 24 * 7 + WARN_METRIC_NUMBER_OF_ACTIVE_USERS_25 = 25 + WARN_METRIC_JOB_WAIT_TIME = 1000 * 3600 * 24 * 7 // 7 days ) type System struct { @@ -83,15 +93,45 @@ func ServerBusyStateFromJson(r io.Reader) *ServerBusyState { } var WarnMetricsTable = map[string]WarnMetric{ + SYSTEM_WARN_METRIC_MFA: { + Id: SYSTEM_WARN_METRIC_MFA, + Limit: -1, + IsBotOnly: true, + IsRunOnce: true, + }, + SYSTEM_WARN_METRIC_EMAIL_DOMAIN: { + Id: SYSTEM_WARN_METRIC_EMAIL_DOMAIN, + Limit: -1, + IsBotOnly: true, + IsRunOnce: true, + }, + SYSTEM_WARN_METRIC_NUMBER_OF_TEAMS_5: { + Id: SYSTEM_WARN_METRIC_NUMBER_OF_TEAMS_5, + Limit: 5, + IsBotOnly: true, + IsRunOnce: true, + }, + SYSTEM_WARN_METRIC_NUMBER_OF_CHANNELS_50: { + Id: SYSTEM_WARN_METRIC_NUMBER_OF_CHANNELS_50, + Limit: 50, + IsBotOnly: true, + IsRunOnce: true, + }, + SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_100: { + Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_100, + Limit: 100, + IsBotOnly: true, + IsRunOnce: true, + }, SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200: { Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200, Limit: 200, IsBotOnly: true, IsRunOnce: true, }, - SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400: { - Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400, - Limit: 400, + SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300: { + Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_300, + Limit: 300, IsBotOnly: true, IsRunOnce: true, }, @@ -101,6 +141,12 @@ var WarnMetricsTable = map[string]WarnMetric{ IsBotOnly: false, IsRunOnce: true, }, + SYSTEM_WARN_METRIC_NUMBER_OF_POSTS_2M: { + Id: SYSTEM_WARN_METRIC_NUMBER_OF_POSTS_2M, + Limit: 2000000, + IsBotOnly: false, + IsRunOnce: true, + }, } type WarnMetric struct { @@ -111,10 +157,10 @@ type WarnMetric struct { } type WarnMetricDisplayTexts struct { - BotTitle string - BotMessageBody string - BotMailToBody string - EmailBody string + BotTitle string + BotMessageBody string + BotSuccessMessage string + EmailBody string } type WarnMetricStatus struct { Id string `json:"id"` diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 5ce771b668..7bbf95841e 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -6550,6 +6550,24 @@ func (s *OpenTracingLayerSystemStore) SaveOrUpdate(system *model.System) error { return err } +func (s *OpenTracingLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.SaveOrUpdateWithWarnMetricHandling") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerSystemStore) Update(system *model.System) error { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SystemStore.Update") @@ -7759,6 +7777,24 @@ func (s *OpenTracingLayerUserStore) AnalyticsActiveCount(time int64, options mod return result, err } +func (s *OpenTracingLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, *model.AppError) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetExternalUsers") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerUserStore) AnalyticsGetGuestCount() (int64, *model.AppError) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.AnalyticsGetGuestCount") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index c3d26f7948..f867e7809b 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -6060,6 +6060,26 @@ func (s *RetryLayerSystemStore) SaveOrUpdate(system *model.System) error { } +func (s *RetryLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error { + + tries := 0 + for { + err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + } + +} + func (s *RetryLayerSystemStore) Update(system *model.System) error { tries := 0 @@ -7364,6 +7384,12 @@ func (s *RetryLayerUserStore) AnalyticsActiveCount(time int64, options model.Use } +func (s *RetryLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, *model.AppError) { + + return s.UserStore.AnalyticsGetExternalUsers(hostDomain) + +} + func (s *RetryLayerUserStore) AnalyticsGetGuestCount() (int64, *model.AppError) { return s.UserStore.AnalyticsGetGuestCount() diff --git a/store/sqlstore/system_store.go b/store/sqlstore/system_store.go index ad756201bb..4ad1197908 100644 --- a/store/sqlstore/system_store.go +++ b/store/sqlstore/system_store.go @@ -6,9 +6,13 @@ package sqlstore import ( "context" "database/sql" + "strconv" + "strings" + "time" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" + "github.com/mattermost/mattermost-server/v5/utils" "github.com/pkg/errors" ) @@ -52,6 +56,26 @@ func (s SqlSystemStore) SaveOrUpdate(system *model.System) error { return nil } +func (s SqlSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error { + if err := s.GetMaster().SelectOne(&model.System{}, "SELECT * FROM Systems WHERE Name = :Name", map[string]interface{}{"Name": system.Name}); err == nil { + if _, err := s.GetMaster().Update(system); err != nil { + return errors.Wrapf(err, "failed to update system property with name=%s", system.Name) + } + } else { + if err := s.GetMaster().Insert(system); err != nil { + return errors.Wrapf(err, "failed to save system property with name=%s", system.Name) + } + } + + if strings.HasPrefix(system.Name, model.WARN_METRIC_STATUS_STORE_PREFIX) && (system.Value == model.WARN_METRIC_STATUS_RUNONCE || system.Value == model.WARN_METRIC_STATUS_LIMIT_REACHED) { + if err := s.SaveOrUpdate(&model.System{Name: model.SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY, Value: strconv.FormatInt(utils.MillisFromTime(time.Now()), 10)}); err != nil { + return errors.Wrapf(err, "failed to save system property with name=%s", model.SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY) + } + } + + return nil +} + func (s SqlSystemStore) Update(system *model.System) error { if _, err := s.GetMaster().Update(system); err != nil { return errors.Wrapf(err, "failed to update system property with name=%s", system.Name) diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index bc49e717fc..0cd3fe3194 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1474,6 +1474,14 @@ func (us SqlUserStore) AnalyticsGetInactiveUsersCount() (int64, *model.AppError) return count, nil } +func (us SqlUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, *model.AppError) { + count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users WHERE LOWER(Email) NOT LIKE :HostDomain", map[string]interface{}{"HostDomain": "%@" + strings.ToLower(hostDomain)}) + if err != nil { + return false, model.NewAppError("SqlUserStore.AnalyticsGetExternalUsers", "store.sql_user.analytics_get_external_users.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return count > 0, nil +} + func (us SqlUserStore) AnalyticsGetGuestCount() (int64, *model.AppError) { count, err := us.GetReplica().SelectInt("SELECT count(*) FROM Users WHERE Roles LIKE :Roles and DeleteAt = 0", map[string]interface{}{"Roles": "%system_guest%"}) if err != nil { diff --git a/store/store.go b/store/store.go index 1568d46b8d..9bb547ed46 100644 --- a/store/store.go +++ b/store/store.go @@ -339,6 +339,7 @@ type UserStore interface { SearchWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) SearchInGroup(groupID string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) AnalyticsGetInactiveUsersCount() (int64, *model.AppError) + AnalyticsGetExternalUsers(hostDomain string) (bool, *model.AppError) AnalyticsGetSystemAdminCount() (int64, *model.AppError) AnalyticsGetGuestCount() (int64, *model.AppError) GetProfilesNotInTeam(teamId string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) @@ -438,6 +439,7 @@ type SystemStore interface { GetByName(name string) (*model.System, error) PermanentDeleteByName(name string) (*model.System, error) InsertIfExists(system *model.System) (*model.System, error) + SaveOrUpdateWithWarnMetricHandling(system *model.System) error } type WebhookStore interface { diff --git a/store/storetest/mocks/SystemStore.go b/store/storetest/mocks/SystemStore.go index 5c7b5cb6e9..48dc2c51c5 100644 --- a/store/storetest/mocks/SystemStore.go +++ b/store/storetest/mocks/SystemStore.go @@ -134,6 +134,20 @@ func (_m *SystemStore) SaveOrUpdate(system *model.System) error { return r0 } +// SaveOrUpdateWithWarnMetricHandling provides a mock function with given fields: system +func (_m *SystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error { + ret := _m.Called(system) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.System) error); ok { + r0 = rf(system) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Update provides a mock function with given fields: system func (_m *SystemStore) Update(system *model.System) error { ret := _m.Called(system) diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 20e838ca62..6094a28067 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -38,6 +38,29 @@ func (_m *UserStore) AnalyticsActiveCount(time int64, options model.UserCountOpt return r0, r1 } +// AnalyticsGetExternalUsers provides a mock function with given fields: hostDomain +func (_m *UserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, *model.AppError) { + ret := _m.Called(hostDomain) + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(hostDomain) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(hostDomain) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // AnalyticsGetGuestCount provides a mock function with given fields: func (_m *UserStore) AnalyticsGetGuestCount() (int64, *model.AppError) { ret := _m.Called() diff --git a/store/storetest/system_store.go b/store/storetest/system_store.go index 2e51c66b8a..5d3267da06 100644 --- a/store/storetest/system_store.go +++ b/store/storetest/system_store.go @@ -21,6 +21,7 @@ func TestSystemStore(t *testing.T, ss store.Store) { t.Run("InsertIfExists", func(t *testing.T) { testInsertIfExists(t, ss) }) + t.Run("SaveOrUpdateWithWarnMetricHandling", func(t *testing.T) { testSystemStoreSaveOrUpdateWithWarnMetricHandling(t, ss) }) } func testSystemStore(t *testing.T, ss store.Store) { @@ -55,6 +56,33 @@ func testSystemStoreSaveOrUpdate(t *testing.T, ss store.Store) { require.Nil(t, err) } +func testSystemStoreSaveOrUpdateWithWarnMetricHandling(t *testing.T, ss store.Store) { + system := &model.System{Name: model.NewId(), Value: "value"} + + err := ss.System().SaveOrUpdateWithWarnMetricHandling(system) + require.Nil(t, err) + + _, err = ss.System().GetByName(model.SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY) + assert.NotNil(t, err) + + system.Name = "warn_metric_number_of_active_users_100" + system.Value = model.WARN_METRIC_STATUS_RUNONCE + err = ss.System().SaveOrUpdateWithWarnMetricHandling(system) + require.Nil(t, err) + + val1, nerr := ss.System().GetByName(model.SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY) + assert.Nil(t, nerr) + + system.Name = "warn_metric_number_of_active_users_100" + system.Value = model.WARN_METRIC_STATUS_ACK + err = ss.System().SaveOrUpdateWithWarnMetricHandling(system) + require.Nil(t, err) + + val2, nerr := ss.System().GetByName(model.SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY) + assert.Nil(t, nerr) + assert.Equal(t, val1, val2) +} + func testSystemStorePermanentDeleteByName(t *testing.T, ss store.Store) { s1 := &model.System{Name: model.NewId(), Value: "value"} s2 := &model.System{Name: model.NewId(), Value: "value"} diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 0ca871ce1b..231d0d7244 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -39,6 +39,7 @@ func TestUserStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("AnalyticsGetInactiveUsersCount", func(t *testing.T) { testUserStoreAnalyticsGetInactiveUsersCount(t, ss) }) t.Run("AnalyticsGetSystemAdminCount", func(t *testing.T) { testUserStoreAnalyticsGetSystemAdminCount(t, ss) }) t.Run("AnalyticsGetGuestCount", func(t *testing.T) { testUserStoreAnalyticsGetGuestCount(t, ss) }) + t.Run("AnalyticsGetExternalUsers", func(t *testing.T) { testUserStoreAnalyticsGetExternalUsers(t, ss) }) t.Run("Save", func(t *testing.T) { testUserStoreSave(t, ss) }) t.Run("Update", func(t *testing.T) { testUserStoreUpdate(t, ss) }) t.Run("UpdateUpdateAt", func(t *testing.T) { testUserStoreUpdateUpdateAt(t, ss) }) @@ -3916,6 +3917,44 @@ func testUserStoreAnalyticsGetGuestCount(t *testing.T, ss store.Store) { require.Equal(t, countBefore+1, result, "Did not get the expected number of guests.") } +func testUserStoreAnalyticsGetExternalUsers(t *testing.T, ss store.Store) { + localHostDomain := "mattermost.com" + result, err := ss.User().AnalyticsGetExternalUsers(localHostDomain) + require.Nil(t, err) + assert.False(t, result) + + u1 := model.User{} + u1.Email = "a@mattermost.com" + u1.Username = model.NewId() + u1.Roles = "system_user system_admin" + + u2 := model.User{} + u2.Email = "b@example.com" + u2.Username = model.NewId() + u2.Roles = "system_user" + + u3 := model.User{} + u3.Email = "c@test.com" + u3.Username = model.NewId() + u3.Roles = "system_guest" + + _, err = ss.User().Save(&u1) + require.Nil(t, err, "couldn't save user") + defer func() { require.Nil(t, ss.User().PermanentDelete(u1.Id)) }() + + _, err = ss.User().Save(&u2) + require.Nil(t, err, "couldn't save user") + defer func() { require.Nil(t, ss.User().PermanentDelete(u2.Id)) }() + + _, err = ss.User().Save(&u3) + require.Nil(t, err, "couldn't save user") + defer func() { require.Nil(t, ss.User().PermanentDelete(u3.Id)) }() + + result, err = ss.User().AnalyticsGetExternalUsers(localHostDomain) + require.Nil(t, err) + assert.True(t, result) +} + func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { team, err := ss.Team().Save(&model.Team{ DisplayName: "Team", diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index a08340c77a..5576905f49 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -5920,6 +5920,22 @@ func (s *TimerLayerSystemStore) SaveOrUpdate(system *model.System) error { return err } +func (s *TimerLayerSystemStore) SaveOrUpdateWithWarnMetricHandling(system *model.System) error { + start := timemodule.Now() + + err := s.SystemStore.SaveOrUpdateWithWarnMetricHandling(system) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("SystemStore.SaveOrUpdateWithWarnMetricHandling", success, elapsed) + } + return err +} + func (s *TimerLayerSystemStore) Update(system *model.System) error { start := timemodule.Now() @@ -7005,6 +7021,22 @@ func (s *TimerLayerUserStore) AnalyticsActiveCount(time int64, options model.Use return result, err } +func (s *TimerLayerUserStore) AnalyticsGetExternalUsers(hostDomain string) (bool, *model.AppError) { + start := timemodule.Now() + + result, err := s.UserStore.AnalyticsGetExternalUsers(hostDomain) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("UserStore.AnalyticsGetExternalUsers", success, elapsed) + } + return result, err +} + func (s *TimerLayerUserStore) AnalyticsGetGuestCount() (int64, *model.AppError) { start := timemodule.Now()