diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | api/user.go | 23 | ||||
-rw-r--r-- | config/config.json | 1 | ||||
-rw-r--r-- | einterfaces/ldap.go | 2 | ||||
-rw-r--r-- | i18n/en.json | 16 | ||||
-rw-r--r-- | mattermost.go | 172 | ||||
-rw-r--r-- | model/config.go | 12 | ||||
-rw-r--r-- | model/job.go | 91 | ||||
-rw-r--r-- | model/job_test.go | 128 | ||||
-rw-r--r-- | store/sql_user_store.go | 21 | ||||
-rw-r--r-- | store/store.go | 1 | ||||
-rw-r--r-- | webapp/components/admin_console/ldap_settings.jsx | 20 | ||||
-rw-r--r-- | webapp/i18n/en.json | 2 |
13 files changed, 416 insertions, 75 deletions
@@ -160,7 +160,7 @@ test-server: start-docker prepare-enterprise echo "mode: count" > cover.out $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=340s -covermode=count -coverprofile=capi.out ./api || exit 1 - $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s -covermode=count -coverprofile=cmodel.out ./model || exit 1 + $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1 $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cweb.out ./web || exit 1 diff --git a/api/user.go b/api/user.go index 4d4518824..de7a560bf 100644 --- a/api/user.go +++ b/api/user.go @@ -704,6 +704,7 @@ func RevokeSessionById(c *Context, sessionId string) { } } +// IF YOU UPDATE THIS PLEASE UPDATE BELOW func RevokeAllSession(c *Context, userId string) { if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil { c.Err = result.Err @@ -726,6 +727,28 @@ func RevokeAllSession(c *Context, userId string) { } } +// UGH... +// If you update this please update above +func RevokeAllSessionsNoContext(userId string) *model.AppError { + if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil { + return result.Err + } else { + sessions := result.Data.([]*model.Session) + + for _, session := range sessions { + if session.IsOAuth { + RevokeAccessToken(session.Token) + } else { + sessionCache.Remove(session.Token) + if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil { + return result.Err + } + } + } + } + return nil +} + func getSessions(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/config/config.json b/config/config.json index 582a7244c..726cb0d8c 100644 --- a/config/config.json +++ b/config/config.json @@ -147,6 +147,7 @@ "UsernameAttribute": "", "NicknameAttribute": "", "IdAttribute": "", + "SyncIntervalMinutes": 60, "SkipCertificateVerification": false, "QueryTimeout": 60, "LoginFieldName": "" diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go index 25d591ce2..4f1b56119 100644 --- a/einterfaces/ldap.go +++ b/einterfaces/ldap.go @@ -13,6 +13,8 @@ type LdapInterface interface { CheckPassword(id string, password string) *model.AppError SwitchToLdap(userId, ldapId, ldapPassword string) *model.AppError ValidateFilter(filter string) *model.AppError + Syncronize() *model.AppError + StartLdapSyncJob() } var theLdapInterface LdapInterface diff --git a/i18n/en.json b/i18n/en.json index 3666abd44..2a51826fa 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2028,6 +2028,14 @@ "translation": "Invalid LDAP Filter" }, { + "id": "ent.ldap.syncronize.get_all.app_error", + "translation": "Unable to get all users using LDAP" + }, + { + "id": "ent.ldap.syncdone.info", + "translation": "LDAP Synchronization completed" + }, + { "id": "ent.mfa.activate.authenticate.app_error", "translation": "Error attempting to authenticate MFA token" }, @@ -2412,6 +2420,10 @@ "translation": "Invalid connection security for LDAP settings. Must be '', 'TLS', or 'STARTTLS'" }, { + "id": "model.config.is_valid.ldap_sync_interval.app_error", + "translation": "Invalid sync interval time. Must be at least one minute." + }, + { "id": "model.config.is_valid.listen_address.app_error", "translation": "Invalid listen address for service settings Must be set." }, @@ -3556,6 +3568,10 @@ "translation": "We encountered an error trying to find the account by authentication type." }, { + "id": "store.sql_user.get_all_using_auth_service.other.app_error", + "translation": "We encountered an error trying to find all the accounts using a specific authentication type." + }, + { "id": "store.sql_user.get_by_username.app_error", "translation": "We couldn't find an existing account matching your username for this team. This team may require an invite from the team owner to join." }, diff --git a/mattermost.go b/mattermost.go index bebb55c3b..ddf20f19e 100644 --- a/mattermost.go +++ b/mattermost.go @@ -55,6 +55,7 @@ var flagCmdPermanentDeleteUser bool var flagCmdPermanentDeleteTeam bool var flagCmdPermanentDeleteAllUsers bool var flagCmdResetDatabase bool +var flagCmdRunLdapSync bool var flagUsername string var flagCmdUploadLicense bool var flagConfigFile string @@ -125,8 +126,12 @@ func main() { setDiagnosticId() go runSecurityAndDiagnosticsJob() - if einterfaces.GetComplianceInterface() != nil { - einterfaces.GetComplianceInterface().StartComplianceDailyJob() + if complianceI := einterfaces.GetComplianceInterface(); complianceI != nil { + complianceI.StartComplianceDailyJob() + } + + if ldapI := einterfaces.GetLdapInterface(); ldapI != nil { + ldapI.StartLdapSyncJob() } // wait for kill signal before attempting to gracefully shutdown @@ -154,96 +159,97 @@ func setDiagnosticId() { } } -func runSecurityAndDiagnosticsJob() { - for { - if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert { - if result := <-api.Srv.Store.System().Get(); result.Err == nil { - props := result.Data.(model.StringMap) - lastSecurityTime, _ := strconv.ParseInt(props[model.SYSTEM_LAST_SECURITY_TIME], 10, 0) - currentTime := model.GetMillis() - - if (currentTime - lastSecurityTime) > 1000*60*60*24*1 { - l4g.Debug(utils.T("mattermost.security_checks.debug")) - - v := url.Values{} - - v.Set(utils.PROP_DIAGNOSTIC_ID, utils.CfgDiagnosticId) - v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber) - v.Set(utils.PROP_DIAGNOSTIC_ENTERPRISE_READY, model.BuildEnterpriseReady) - v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName) - v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS) - v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT) - - if len(props[model.SYSTEM_RAN_UNIT_TESTS]) > 0 { - v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "1") - } else { - v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "0") - } +func doSecurityAndDiagnostics() { + if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert { + if result := <-api.Srv.Store.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + lastSecurityTime, _ := strconv.ParseInt(props[model.SYSTEM_LAST_SECURITY_TIME], 10, 0) + currentTime := model.GetMillis() + + if (currentTime - lastSecurityTime) > 1000*60*60*24*1 { + l4g.Debug(utils.T("mattermost.security_checks.debug")) + + v := url.Values{} + + v.Set(utils.PROP_DIAGNOSTIC_ID, utils.CfgDiagnosticId) + v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber) + v.Set(utils.PROP_DIAGNOSTIC_ENTERPRISE_READY, model.BuildEnterpriseReady) + v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName) + v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS) + v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT) + + if len(props[model.SYSTEM_RAN_UNIT_TESTS]) > 0 { + v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "1") + } else { + v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "0") + } - systemSecurityLastTime := &model.System{Name: model.SYSTEM_LAST_SECURITY_TIME, Value: strconv.FormatInt(currentTime, 10)} - if lastSecurityTime == 0 { - <-api.Srv.Store.System().Save(systemSecurityLastTime) - } else { - <-api.Srv.Store.System().Update(systemSecurityLastTime) - } + systemSecurityLastTime := &model.System{Name: model.SYSTEM_LAST_SECURITY_TIME, Value: strconv.FormatInt(currentTime, 10)} + if lastSecurityTime == 0 { + <-api.Srv.Store.System().Save(systemSecurityLastTime) + } else { + <-api.Srv.Store.System().Update(systemSecurityLastTime) + } - if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { - v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) - } + if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) + } - if ucr := <-api.Srv.Store.User().GetTotalActiveUsersCount(); ucr.Err == nil { - v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) - } + if ucr := <-api.Srv.Store.User().GetTotalActiveUsersCount(); ucr.Err == nil { + v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10)) + } - res, err := http.Get(utils.DIAGNOSTIC_URL + "/security?" + v.Encode()) - if err != nil { - l4g.Error(utils.T("mattermost.security_info.error")) - return - } + res, err := http.Get(utils.DIAGNOSTIC_URL + "/security?" + v.Encode()) + if err != nil { + l4g.Error(utils.T("mattermost.security_info.error")) + return + } + + bulletins := model.SecurityBulletinsFromJson(res.Body) + + for _, bulletin := range bulletins { + if bulletin.AppliesToVersion == model.CurrentVersion { + if props["SecurityBulletin_"+bulletin.Id] == "" { + if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil { + l4g.Error(utils.T("mattermost.system_admins.error")) + return + } else { + users := results.Data.(map[string]*model.User) - bulletins := model.SecurityBulletinsFromJson(res.Body) + resBody, err := http.Get(utils.DIAGNOSTIC_URL + "/bulletins/" + bulletin.Id) + if err != nil { + l4g.Error(utils.T("mattermost.security_bulletin.error")) + return + } - for _, bulletin := range bulletins { - if bulletin.AppliesToVersion == model.CurrentVersion { - if props["SecurityBulletin_"+bulletin.Id] == "" { - if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil { - l4g.Error(utils.T("mattermost.system_admins.error")) + body, err := ioutil.ReadAll(resBody.Body) + res.Body.Close() + if err != nil || resBody.StatusCode != 200 { + l4g.Error(utils.T("mattermost.security_bulletin_read.error")) return - } else { - users := results.Data.(map[string]*model.User) - - resBody, err := http.Get(utils.DIAGNOSTIC_URL + "/bulletins/" + bulletin.Id) - if err != nil { - l4g.Error(utils.T("mattermost.security_bulletin.error")) - return - } - - body, err := ioutil.ReadAll(resBody.Body) - res.Body.Close() - if err != nil || resBody.StatusCode != 200 { - l4g.Error(utils.T("mattermost.security_bulletin_read.error")) - return - } - - for _, user := range users { - l4g.Info(utils.T("mattermost.send_bulletin.info"), bulletin.Id, user.Email) - utils.SendMail(user.Email, utils.T("mattermost.bulletin.subject"), string(body)) - } } - bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id} - <-api.Srv.Store.System().Save(bulletinSeen) + for _, user := range users { + l4g.Info(utils.T("mattermost.send_bulletin.info"), bulletin.Id, user.Email) + utils.SendMail(user.Email, utils.T("mattermost.bulletin.subject"), string(body)) + } } + + bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id} + <-api.Srv.Store.System().Save(bulletinSeen) } } } } } - - time.Sleep(time.Hour * 4) } } +func runSecurityAndDiagnosticsJob() { + doSecurityAndDiagnostics() + model.CreateRecurringTask("Security and Diagnostics", doSecurityAndDiagnostics, time.Hour*4) +} + func parseCmds() { flag.Usage = func() { fmt.Fprintln(os.Stderr, usage) @@ -272,6 +278,7 @@ func parseCmds() { flag.BoolVar(&flagCmdPermanentDeleteTeam, "permanent_delete_team", false, "") flag.BoolVar(&flagCmdPermanentDeleteAllUsers, "permanent_delete_all_users", false, "") flag.BoolVar(&flagCmdResetDatabase, "reset_database", false, "") + flag.BoolVar(&flagCmdRunLdapSync, "ldap_sync", false, "") flag.BoolVar(&flagCmdUploadLicense, "upload_license", false, "") flag.Parse() @@ -290,6 +297,7 @@ func parseCmds() { flagCmdPermanentDeleteTeam || flagCmdPermanentDeleteAllUsers || flagCmdResetDatabase || + flagCmdRunLdapSync || flagCmdUploadLicense) } @@ -308,6 +316,7 @@ func runCmds() { cmdPermDeleteAllUsers() cmdResetDatabase() cmdUploadLicense() + cmdRunLdapSync() } type TeamForUpgrade struct { @@ -1130,6 +1139,21 @@ func cmdResetDatabase() { } +func cmdRunLdapSync() { + if flagCmdRunLdapSync { + if ldapI := einterfaces.GetLdapInterface(); ldapI != nil { + if err := ldapI.Syncronize(); err != nil { + fmt.Println("ERROR: Ldap Syncronization Failed") + l4g.Error("%v", err.Error()) + flushLogAndExit(1) + } else { + fmt.Println("SUCCESS: Ldap Syncronization Complete") + flushLogAndExit(0) + } + } + } +} + func cmdUploadLicense() { if flagCmdUploadLicense { if model.BuildEnterpriseReady != "true" { diff --git a/model/config.go b/model/config.go index 08b00b90f..7e810be02 100644 --- a/model/config.go +++ b/model/config.go @@ -190,6 +190,9 @@ type LdapSettings struct { NicknameAttribute *string IdAttribute *string + // Syncronization + SyncIntervalMinutes *int + // Advanced SkipCertificateVerification *bool QueryTimeout *int @@ -441,6 +444,11 @@ func (o *Config) SetDefaults() { *o.LdapSettings.LoginFieldName = "" } + if o.LdapSettings.SyncIntervalMinutes == nil { + o.LdapSettings.SyncIntervalMinutes = new(int) + *o.LdapSettings.SyncIntervalMinutes = 60 + } + if o.ServiceSettings.SessionLengthWebInDays == nil { o.ServiceSettings.SessionLengthWebInDays = new(int) *o.ServiceSettings.SessionLengthWebInDays = 30 @@ -635,6 +643,10 @@ func (o *Config) IsValid() *AppError { return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_security.app_error", nil, "") } + if *o.LdapSettings.SyncIntervalMinutes <= 0 { + return NewLocAppError("Config.IsValid", "model.config.is_valid.ldap_sync_interval.app_error", nil, "") + } + return nil } diff --git a/model/job.go b/model/job.go new file mode 100644 index 000000000..bcae7a830 --- /dev/null +++ b/model/job.go @@ -0,0 +1,91 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "fmt" + "time" +) + +type TaskFunc func() + +type ScheduledTask struct { + Name string `json:"name"` + Interval time.Duration `json:"interval"` + Recurring bool `json:"recurring"` + function TaskFunc `json:",omitempty"` + timer *time.Timer `json:",omitempty"` +} + +var tasks = make(map[string]*ScheduledTask) + +func addTask(task *ScheduledTask) { + tasks[task.Name] = task +} + +func removeTaskByName(name string) { + delete(tasks, name) +} + +func getTaskByName(name string) *ScheduledTask { + return tasks[name] +} + +func GetAllTasks() *map[string]*ScheduledTask { + return &tasks +} + +func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { + task := &ScheduledTask{ + Name: name, + Interval: timeToExecution, + Recurring: false, + function: function, + } + + taskRunner := func() { + go task.function() + removeTaskByName(task.Name) + } + + task.timer = time.AfterFunc(timeToExecution, taskRunner) + + addTask(task) + + return task +} + +func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { + task := &ScheduledTask{ + Name: name, + Interval: interval, + Recurring: true, + function: function, + } + + taskRecurer := func() { + go task.function() + task.timer.Reset(task.Interval) + } + + task.timer = time.AfterFunc(interval, taskRecurer) + + addTask(task) + + return task +} + +func (task *ScheduledTask) Cancel() { + task.timer.Stop() + removeTaskByName(task.Name) +} + +func (task *ScheduledTask) String() string { + return fmt.Sprintf( + "%s\nInterval: %s\nRecurring: %t\n", + task.Name, + task.Interval.String(), + task.Recurring, + ) +} diff --git a/model/job_test.go b/model/job_test.go new file mode 100644 index 000000000..2a307de1e --- /dev/null +++ b/model/job_test.go @@ -0,0 +1,128 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "testing" + "time" +) + +func TestCreateTask(t *testing.T) { + TASK_NAME := "Test Task" + TASK_TIME := time.Second * 3 + + testValue := 0 + testFunc := func() { + testValue = 1 + } + + task := CreateTask(TASK_NAME, testFunc, TASK_TIME) + if testValue != 0 { + t.Fatal("Unexpected execuition of task") + } + + time.Sleep(TASK_TIME + time.Second) + + if testValue != 1 { + t.Fatal("Task did not execute") + } + + if task.Name != TASK_NAME { + t.Fatal("Bad name") + } + + if task.Interval != TASK_TIME { + t.Fatal("Bad interval") + } + + if task.Recurring != false { + t.Fatal("should not reccur") + } +} + +func TestCreateRecurringTask(t *testing.T) { + TASK_NAME := "Test Recurring Task" + TASK_TIME := time.Second * 3 + + testValue := 0 + testFunc := func() { + testValue += 1 + } + + task := CreateRecurringTask(TASK_NAME, testFunc, TASK_TIME) + if testValue != 0 { + t.Fatal("Unexpected execuition of task") + } + + time.Sleep(TASK_TIME + time.Second) + + if testValue != 1 { + t.Fatal("Task did not execute") + } + + time.Sleep(TASK_TIME) + + if testValue != 2 { + t.Fatal("Task did not re-execute") + } + + if task.Name != TASK_NAME { + t.Fatal("Bad name") + } + + if task.Interval != TASK_TIME { + t.Fatal("Bad interval") + } + + if task.Recurring != true { + t.Fatal("should reccur") + } + + task.Cancel() +} + +func TestCancelTask(t *testing.T) { + TASK_NAME := "Test Task" + TASK_TIME := time.Second * 3 + + testValue := 0 + testFunc := func() { + testValue = 1 + } + + task := CreateTask(TASK_NAME, testFunc, TASK_TIME) + if testValue != 0 { + t.Fatal("Unexpected execuition of task") + } + task.Cancel() + + time.Sleep(TASK_TIME + time.Second) + + if testValue != 0 { + t.Fatal("Unexpected execuition of task") + } +} + +func TestGetAllTasks(t *testing.T) { + doNothing := func() {} + + CreateTask("Task1", doNothing, time.Hour) + CreateTask("Task2", doNothing, time.Second) + CreateRecurringTask("Task3", doNothing, time.Second) + task4 := CreateRecurringTask("Task4", doNothing, time.Second) + + task4.Cancel() + + time.Sleep(time.Second * 3) + + tasks := *GetAllTasks() + if len(tasks) != 2 { + t.Fatal("Wrong number of tasks got: ", len(tasks)) + } + for _, task := range tasks { + if task.Name != "Task1" && task.Name != "Task3" { + t.Fatal("Wrong tasks") + } + } +} diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 11a915055..07a801dc6 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -793,6 +793,27 @@ func (us SqlUserStore) GetByAuth(authData *string, authService string) StoreChan return storeChannel } +func (us SqlUserStore) GetAllUsingAuthService(authService string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + var data []*model.User + + if _, err := us.GetReplica().Select(&data, "SELECT * FROM Users WHERE AuthService = :AuthService", map[string]interface{}{"AuthService": authService}); err != nil { + result.Err = model.NewLocAppError("SqlUserStore.GetByAuth", "store.sql_user.get_by_auth.other.app_error", nil, "authService="+authService+", "+err.Error()) + } + + result.Data = data + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlUserStore) GetByUsername(username string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/store.go b/store/store.go index ebbd2e454..7f4db396c 100644 --- a/store/store.go +++ b/store/store.go @@ -139,6 +139,7 @@ type UserStore interface { GetProfileByIds(userId []string) StoreChannel GetByEmail(email string) StoreChannel GetByAuth(authData *string, authService string) StoreChannel + GetAllUsingAuthService(authService string) StoreChannel GetByUsername(username string) StoreChannel GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled bool) StoreChannel VerifyEmail(userId string) StoreChannel diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx index e4fd7f6cc..fb121f656 100644 --- a/webapp/components/admin_console/ldap_settings.jsx +++ b/webapp/components/admin_console/ldap_settings.jsx @@ -36,6 +36,7 @@ export default class LdapSettings extends AdminSettings { emailAttribute: props.config.LdapSettings.EmailAttribute, usernameAttribute: props.config.LdapSettings.UsernameAttribute, idAttribute: props.config.LdapSettings.IdAttribute, + syncIntervalMinutes: props.config.LdapSettings.SyncIntervalMinutes, skipCertificateVerification: props.config.LdapSettings.SkipCertificateVerification, queryTimeout: props.config.LdapSettings.QueryTimeout, loginFieldName: props.config.LdapSettings.LoginFieldName @@ -57,6 +58,7 @@ export default class LdapSettings extends AdminSettings { config.LdapSettings.EmailAttribute = this.state.emailAttribute; config.LdapSettings.UsernameAttribute = this.state.usernameAttribute; config.LdapSettings.IdAttribute = this.state.idAttribute; + config.LdapSettings.SyncIntervalMinutes = this.parseIntNonZero(this.state.syncIntervalMinutes); config.LdapSettings.SkipCertificateVerification = this.state.skipCertificateVerification; config.LdapSettings.QueryTimeout = this.parseIntNonZero(this.state.queryTimeout); config.LdapSettings.LoginFieldName = this.state.loginFieldName; @@ -339,6 +341,24 @@ export default class LdapSettings extends AdminSettings { onChange={this.handleChange} disabled={!this.state.enable} /> + <TextSetting + id='syncIntervalMinutes' + label={ + <FormattedMessage + id='admin.ldap.syncIntervalTitle' + defaultMessage='Synchronization Interval (In Minutes)' + /> + } + helpText={ + <FormattedMessage + id='admin.ldap.syncIntervalHelpText' + defaultMessage='LDAP Synchronization is the process by which Mattermost updates its users to reflect any updated data on the LDAP server. For example if a name for a user is updated on the LDAP server, the change will be reflected in Mattermost when the synchronization is performed. Accounts that have been removed from the LDAP server will have their active sessions cleared and no longer be able to login to Mattermost. Mattermost will perform this synchronization regularly according to the interval supplied here. For example, if 60 is supplied, Mattermost will update the users every hour.' + /> + } + value={this.state.syncIntervalMinutes} + onChange={this.handleChange} + disabled={!this.state.enable} + /> <BooleanSetting id='skipCertificateVerification' label={ diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 089e3299f..1e1177ec0 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -286,6 +286,8 @@ "admin.ldap.userFilterTitle": "User Filter:", "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Username Attribute:", + "admin.ldap.syncIntervalTitle": "Synchronization Interval (In Minutes)", + "admin.ldap.syncIntervalHelpText": "LDAP Synchronization is the process by which Mattermost updates its users to reflect any updated data on the LDAP server. For example if a name for a user is updated on the LDAP server, the change will be reflected in Mattermost when the synchronization is performed. Accounts that have been removed from the LDAP server will have their active sessions cleared and no longer be able to login to Mattermost. Mattermost will perform this synchronization regularly according to the interval supplied here. For example, if 60 is supplied, Mattermost will update the users every hour.", "admin.license.choose": "Choose File", "admin.license.chooseFile": "Choose File", "admin.license.edition": "Edition: ", |