summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/config.json2
-rw-r--r--docker/dev/config_docker.json2
-rw-r--r--docker/local/config_docker.json2
-rw-r--r--mattermost.go99
-rw-r--r--model/config.go6
-rw-r--r--model/security_bulletin.go55
-rw-r--r--store/sql_user_store.go31
-rw-r--r--store/sql_user_store_test.go23
-rw-r--r--store/store.go1
-rw-r--r--utils/diagnostic.go14
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx16
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>