Этот коммит содержится в:
Christopher Speller
2016-06-03 09:33:59 -04:00
коммит произвёл Joram Wilander
родитель 87f357a54d
Коммит ea3342aa6c
13 изменённых файлов: 412 добавлений и 71 удалений

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -147,6 +147,7 @@
"UsernameAttribute": "",
"NicknameAttribute": "",
"IdAttribute": "",
"SyncIntervalMinutes": 60,
"SkipCertificateVerification": false,
"QueryTimeout": 60,
"LoginFieldName": ""

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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."

Просмотреть файл

@@ -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" {

Просмотреть файл

@@ -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
}

91
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,
)
}

128
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")
}
}
}

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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={

Просмотреть файл

@@ -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: ",