diff --git a/Makefile b/Makefile
index 1d808fd750..adf201ec20 100644
--- a/Makefile
+++ b/Makefile
@@ -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 4d4518824b..de7a560bf3 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 582a7244ca..726cb0d8cb 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 25d591ce20..4f1b56119e 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 3666abd447..2a51826fad 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -2027,6 +2027,14 @@
"id": "ent.ldap.validate_filter.app_error",
"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"
@@ -2411,6 +2419,10 @@
"id": "model.config.is_valid.ldap_security.app_error",
"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."
@@ -3555,6 +3567,10 @@
"id": "store.sql_user.get_by_auth.other.app_error",
"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 bebb55c3bc..ddf20f19ec 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()
+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"))
+ if (currentTime - lastSecurityTime) > 1000*60*60*24*1 {
+ l4g.Debug(utils.T("mattermost.security_checks.debug"))
- v := url.Values{}
+ 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)
+ 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")
- }
+ 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)
+ 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"))
+ for _, bulletin := range bulletins {
+ if bulletin.AppliesToVersion == model.CurrentVersion {
+ if props["SecurityBulletin_"+bulletin.Id] == "" {
+ if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil {
+ l4g.Error(utils.T("mattermost.system_admins.error"))
+ return
+ } else {
+ users := results.Data.(map[string]*model.User)
+
+ resBody, err := http.Get(utils.DIAGNOSTIC_URL + "/bulletins/" + bulletin.Id)
+ if err != nil {
+ l4g.Error(utils.T("mattermost.security_bulletin.error"))
return
- } 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)
+ 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)
}
}
}
}
}
-
- 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 08b00b90f6..7e810be02f 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 0000000000..bcae7a830b
--- /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 0000000000..2a307de1ef
--- /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 11a9150559..07a801dc6d 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 ebbd2e454b..7f4db396c9 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 e4fd7f6cc8..fb121f656c 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}
/>
+
+ }
+ helpText={
+
+ }
+ value={this.state.syncIntervalMinutes}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />