diff options
-rw-r--r-- | config/config.json | 2 | ||||
-rw-r--r-- | docker/dev/config_docker.json | 2 | ||||
-rw-r--r-- | docker/local/config_docker.json | 2 | ||||
-rw-r--r-- | mattermost.go | 99 | ||||
-rw-r--r-- | model/config.go | 6 | ||||
-rw-r--r-- | model/security_bulletin.go | 55 | ||||
-rw-r--r-- | store/sql_user_store.go | 31 | ||||
-rw-r--r-- | store/sql_user_store_test.go | 23 | ||||
-rw-r--r-- | store/store.go | 1 | ||||
-rw-r--r-- | utils/diagnostic.go | 14 | ||||
-rw-r--r-- | web/react/components/admin_console/privacy_settings.jsx | 16 |
11 files changed, 203 insertions, 48 deletions
diff --git a/config/config.json b/config/config.json index b14175372..02c59d825 100644 --- a/config/config.json +++ b/config/config.json @@ -78,7 +78,7 @@ "PrivacySettings": { "ShowEmailAddress": true, "ShowFullName": true, - "EnableDiagnostic": false + "EnableSecurityFixAlert": true }, "GitLabSettings": { "Enable": false, diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index ef91a21ea..ab5b0a7be 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -78,7 +78,7 @@ "PrivacySettings": { "ShowEmailAddress": true, "ShowFullName": true, - "EnableDiagnostic": false + "EnableSecurityFixAlert": true }, "GitLabSettings": { "Enable": false, diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index ef91a21ea..ab5b0a7be 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -78,7 +78,7 @@ "PrivacySettings": { "ShowEmailAddress": true, "ShowFullName": true, - "EnableDiagnostic": false + "EnableSecurityFixAlert": true }, "GitLabSettings": { "Enable": false, diff --git a/mattermost.go b/mattermost.go index e78e8d04a..0d8aebc76 100644 --- a/mattermost.go +++ b/mattermost.go @@ -6,6 +6,8 @@ package main import ( "flag" "fmt" + "io/ioutil" + "net/http" "os" "os/signal" "runtime" @@ -63,7 +65,7 @@ func main() { manualtesting.InitManualTesting() } - diagnosticsJob() + securityAndDiagnosticsJob() // wait for kill signal before attempting to gracefully shutdown // the running service @@ -75,49 +77,94 @@ func main() { } } -func diagnosticsJob() { +func securityAndDiagnosticsJob() { go func() { for { - if utils.Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() { + if utils.Cfg.PrivacySettings.EnableSecurityFixAlert { //&& model.IsOfficalBuild() { if result := <-api.Srv.Store.System().Get(); result.Err == nil { props := result.Data.(model.StringMap) - lastTime, _ := strconv.ParseInt(props["LastDiagnosticTime"], 10, 0) + lastSecurityTime, _ := strconv.ParseInt(props["LastSecurityTime"], 10, 0) currentTime := model.GetMillis() - if (currentTime - lastTime) > 1000*60*60*24*7 { - l4g.Info("Sending error and diagnostic information to mattermost") + id := props["DiagnosticId"] + if len(id) == 0 { + id = model.NewId() + systemId := &model.System{Name: "DiagnosticId", Value: id} + <-api.Srv.Store.System().Save(systemId) + } - id := props["DiagnosticId"] - if len(id) == 0 { - id = model.NewId() - systemId := &model.System{Name: "DiagnosticId", Value: id} - <-api.Srv.Store.System().Save(systemId) - } + m := make(map[string]string) + m[utils.PROP_DIAGNOSTIC_ID] = id + m[utils.PROP_DIAGNOSTIC_BUILD] = model.CurrentVersion + "." + model.BuildNumber + m[utils.PROP_DIAGNOSTIC_DATABASE] = utils.Cfg.SqlSettings.DriverName + m[utils.PROP_DIAGNOSTIC_OS] = runtime.GOOS + m[utils.PROP_DIAGNOSTIC_CATEGORY] = utils.VAL_DIAGNOSTIC_CATEGORY_DEFALUT + + if (currentTime - lastSecurityTime) > 1000*60*60*24*1 { + l4g.Info("Checking for security update from Mattermost") - systemLastTime := &model.System{Name: "LastDiagnosticTime", Value: strconv.FormatInt(currentTime, 10)} - if lastTime == 0 { - <-api.Srv.Store.System().Save(systemLastTime) + systemSecurityLastTime := &model.System{Name: "LastSecurityTime", Value: strconv.FormatInt(currentTime, 10)} + if lastSecurityTime == 0 { + <-api.Srv.Store.System().Save(systemSecurityLastTime) } else { - <-api.Srv.Store.System().Update(systemLastTime) + <-api.Srv.Store.System().Update(systemSecurityLastTime) } - m := make(map[string]string) - m[utils.PROP_DIAGNOSTIC_ID] = id - m[utils.PROP_DIAGNOSTIC_BUILD] = model.CurrentVersion + "." + model.BuildNumber - m[utils.PROP_DIAGNOSTIC_DATABASE] = utils.Cfg.SqlSettings.DriverName - m[utils.PROP_DIAGNOSTIC_OS] = runtime.GOOS - m[utils.PROP_DIAGNOSTIC_CATEGORY] = utils.VAL_DIAGNOSTIC_CATEGORY_DEFALUT + query := "?" + for name, value := range m { + if len(query) > 1 { + query += "&" + } - if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { - m[utils.PROP_DIAGNOSTIC_USER_COUNT] = strconv.FormatInt(ucr.Data.(int64), 10) + query += name + "=" + utils.UrlEncode(value) } - utils.SendDiagnostic(m) + res, err := http.Get(utils.DIAGNOSTIC_URL + "/security" + query) + if err != nil { + l4g.Error("Failed to get security update information from Mattermost.") + 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("Failed to get system admins for security update information from Mattermost.") + return + } else { + users := results.Data.(map[string]*model.User) + + resBody, err := http.Get(utils.DIAGNOSTIC_URL + "/bulletins/" + bulletin.Id) + if err != nil { + l4g.Error("Failed to get security bulletin details") + return + } + + body, err := ioutil.ReadAll(resBody.Body) + res.Body.Close() + if err != nil || resBody.StatusCode != 200 { + l4g.Error("Failed to read security bulletin details") + return + } + + for _, user := range users { + l4g.Info("Sending security bulletin for " + bulletin.Id + " to " + user.Email) + utils.SendMail(user.Email, "Mattermost Security Bulletin", string(body)) + } + } + + bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id} + <-api.Srv.Store.System().Save(bulletinSeen) + } + } + } } } } - time.Sleep(time.Hour * 24) + time.Sleep(time.Hour * 4) } }() } diff --git a/model/config.go b/model/config.go index c67b36063..086b0d4ee 100644 --- a/model/config.go +++ b/model/config.go @@ -110,9 +110,9 @@ type RateLimitSettings struct { } type PrivacySettings struct { - ShowEmailAddress bool - ShowFullName bool - EnableDiagnostic bool + ShowEmailAddress bool + ShowFullName bool + EnableSecurityFixAlert bool } type TeamSettings struct { diff --git a/model/security_bulletin.go b/model/security_bulletin.go new file mode 100644 index 000000000..a64e03f6d --- /dev/null +++ b/model/security_bulletin.go @@ -0,0 +1,55 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SecurityBulletin struct { + Id string `json:"id"` + AppliesToVersion string `json:"applies_to_version"` +} + +type SecurityBulletins []SecurityBulletin + +func (me *SecurityBulletin) ToJson() string { + b, err := json.Marshal(me) + if err != nil { + return "" + } else { + return string(b) + } +} + +func SecurityBulletinFromJson(data io.Reader) *SecurityBulletin { + decoder := json.NewDecoder(data) + var o SecurityBulletin + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (me SecurityBulletins) ToJson() string { + if b, err := json.Marshal(me); err != nil { + return "[]" + } else { + return string(b) + } +} + +func SecurityBulletinsFromJson(data io.Reader) SecurityBulletins { + decoder := json.NewDecoder(data) + var o SecurityBulletins + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 0a723d965..f82f87290 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -370,6 +370,37 @@ func (us SqlUserStore) GetProfiles(teamId string) StoreChannel { return storeChannel } +func (us SqlUserStore) GetSystemAdminProfiles() StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var users []*model.User + + if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE Roles = :Roles", map[string]interface{}{"Roles": "system_admin"}); err != nil { + result.Err = model.NewAppError("SqlUserStore.GetSystemAdminProfiles", "We encounted an error while finding user profiles", err.Error()) + } else { + + userMap := make(map[string]*model.User) + + for _, u := range users { + u.Password = "" + u.AuthData = "" + userMap[u.Id] = u + } + + result.Data = userMap + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index e2a454023..bfdd14fef 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -259,6 +259,29 @@ func TestUserStoreGetProfiles(t *testing.T) { } } +func TestUserStoreGetSystemAdminProfiles(t *testing.T) { + Setup() + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + Must(store.User().Save(&u1)) + + u2 := model.User{} + u2.TeamId = u1.TeamId + u2.Email = model.NewId() + Must(store.User().Save(&u2)) + + if r1 := <-store.User().GetSystemAdminProfiles(); r1.Err != nil { + t.Fatal(r1.Err) + } else { + users := r1.Data.(map[string]*model.User) + if len(users) <= 0 { + t.Fatal("invalid returned system admin users") + } + } +} + func TestUserStoreGetByEmail(t *testing.T) { Setup() diff --git a/store/store.go b/store/store.go index 887913bc6..fc088ce74 100644 --- a/store/store.go +++ b/store/store.go @@ -104,6 +104,7 @@ type UserStore interface { UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel GetForExport(teamId string) StoreChannel GetTotalUsersCount() StoreChannel + GetSystemAdminProfiles() StoreChannel } type SessionStore interface { diff --git a/utils/diagnostic.go b/utils/diagnostic.go index 9a61ae934..8ff2922f4 100644 --- a/utils/diagnostic.go +++ b/utils/diagnostic.go @@ -6,12 +6,12 @@ package utils import ( "net/http" - l4g "code.google.com/p/log4go" - "github.com/mattermost/platform/model" ) const ( + DIAGNOSTIC_URL = "https://d7zmvsa9e04kk.cloudfront.net" + PROP_DIAGNOSTIC_ID = "id" PROP_DIAGNOSTIC_CATEGORY = "c" VAL_DIAGNOSTIC_CATEGORY_DEFALUT = "d" @@ -21,8 +21,8 @@ const ( PROP_DIAGNOSTIC_USER_COUNT = "uc" ) -func SendDiagnostic(data model.StringMap) *model.AppError { - if Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() { +func SendDiagnostic(data model.StringMap) { + if Cfg.PrivacySettings.EnableSecurityFixAlert && model.IsOfficalBuild() { query := "?" for name, value := range data { @@ -33,13 +33,11 @@ func SendDiagnostic(data model.StringMap) *model.AppError { query += name + "=" + UrlEncode(value) } - res, err := http.Get("http://d7zmvsa9e04kk.cloudfront.net/i" + query) + res, err := http.Get(DIAGNOSTIC_URL + "/i" + query) if err != nil { - l4g.Error("Failed to send diagnostics %v", err.Error()) + return } res.Body.Close() } - - return nil } diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx index c74d321e6..3467e6a40 100644 --- a/web/react/components/admin_console/privacy_settings.jsx +++ b/web/react/components/admin_console/privacy_settings.jsx @@ -30,7 +30,7 @@ export default class PrivacySettings extends React.Component { var config = this.props.config; config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked; config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked; - config.PrivacySettings.EnableDiagnostic = React.findDOMNode(this.refs.EnableDiagnostic).checked; + config.PrivacySettings.EnableSecurityFixAlert = React.findDOMNode(this.refs.EnableSecurityFixAlert).checked; Client.saveConfig( config, @@ -140,7 +140,7 @@ export default class PrivacySettings extends React.Component { <div className='form-group'> <label className='control-label col-sm-4' - htmlFor='EnableDiagnostic' + htmlFor='EnableSecurityFixAlert' > {'Send Error and Diagnostic: '} </label> @@ -148,10 +148,10 @@ export default class PrivacySettings extends React.Component { <label className='radio-inline'> <input type='radio' - name='EnableDiagnostic' + name='EnableSecurityFixAlert' value='true' - ref='EnableDiagnostic' - defaultChecked={this.props.config.PrivacySettings.EnableDiagnostic} + ref='EnableSecurityFixAlert' + defaultChecked={this.props.config.PrivacySettings.EnableSecurityFixAlert} onChange={this.handleChange} /> {'true'} @@ -159,14 +159,14 @@ export default class PrivacySettings extends React.Component { <label className='radio-inline'> <input type='radio' - name='EnableDiagnostic' + name='EnableSecurityFixAlert' value='false' - defaultChecked={!this.props.config.PrivacySettings.EnableDiagnostic} + defaultChecked={!this.props.config.PrivacySettings.EnableSecurityFixAlert} onChange={this.handleChange} /> {'false'} </label> - <p className='help-text'>{'When true, The server will periodically send error and diagnostic information to Mattermost.'}</p> + <p className='help-text'>{'When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'}</p> </div> </div> |