From 6d21a339dc3aedd373faacd5163462c76263ab07 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Thu, 10 Mar 2016 09:39:05 -0800 Subject: PLT-2115 adding compliance feature to enterprise --- i18n/en.json | 4 ++ model/compliance_post.go | 92 +++++++++++++++++++++++++++++++++++++++++++ model/compliance_post_test.go | 27 +++++++++++++ store/sql_post_store.go | 56 ++++++++++++++++++++++++++ store/sql_post_store_test.go | 76 +++++++++++++++++++++++++++++++++++ store/store.go | 1 + 6 files changed, 256 insertions(+) create mode 100644 model/compliance_post.go create mode 100644 model/compliance_post_test.go diff --git a/i18n/en.json b/i18n/en.json index b7ebbb8b1..20d997af7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2771,6 +2771,10 @@ "id": "store.sql_post.analytics_posts_count.app_error", "translation": "We couldn't get post counts" }, + { + "id": "store.sql_post.compliance_export.app_error", + "translation": "We couldn't get posts for compliance export" + }, { "id": "store.sql_post.analytics_posts_count_by_day.app_error", "translation": "We couldn't get post counts by day" diff --git a/model/compliance_post.go b/model/compliance_post.go new file mode 100644 index 000000000..636be8f17 --- /dev/null +++ b/model/compliance_post.go @@ -0,0 +1,92 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "time" +) + +type CompliancePost struct { + + // From Team + TeamName string + TeamDisplayName string + + // From Channel + ChannelName string + ChannelDisplayName string + + // From User + UserUsername string + UserEmail string + UserNickname string + + // From Post + PostId string + PostCreateAt int64 + PostUpdateAt int64 + PostDeleteAt int64 + PostRootId string + PostParentId string + PostOriginalId string + PostMessage string + PostType string + PostProps string + PostHashtags string + PostFilenames string +} + +func CompliancePostHeader() []string { + return []string{ + "TeamName", + "TeamDisplayName", + + "ChannelName", + "ChannelDisplayName", + + "UserUsername", + "UserEmail", + "UserNickname", + + "PostId", + "PostCreateAt", + "PostUpdateAt", + "PostDeleteAt", + "PostRootId", + "PostParentId", + "PostOriginalId", + "PostMessage", + "PostType", + "PostProps", + "PostHashtags", + "PostFilenames", + } +} + +func (me *CompliancePost) Row() []string { + return []string{ + me.TeamName, + me.TeamDisplayName, + + me.ChannelName, + me.ChannelDisplayName, + + me.UserUsername, + me.UserEmail, + me.UserNickname, + + me.PostId, + time.Unix(0, me.PostCreateAt*1000).Format(time.RFC3339), + time.Unix(0, me.PostUpdateAt*1000).Format(time.RFC3339), + time.Unix(0, me.PostDeleteAt*1000).Format(time.RFC3339), + me.PostRootId, + me.PostParentId, + me.PostOriginalId, + me.PostMessage, + me.PostType, + me.PostProps, + me.PostHashtags, + me.PostFilenames, + } +} diff --git a/model/compliance_post_test.go b/model/compliance_post_test.go new file mode 100644 index 000000000..28e20ba4b --- /dev/null +++ b/model/compliance_post_test.go @@ -0,0 +1,27 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "testing" +) + +func TestCompliancePostHeader(t *testing.T) { + if CompliancePostHeader()[0] != "TeamName" { + t.Fatal() + } +} + +func TestCompliancePost(t *testing.T) { + o := CompliancePost{TeamName: "test", PostFilenames: "files", PostCreateAt: GetMillis()} + r := o.Row() + + if r[0] != "test" { + t.Fatal() + } + + if r[len(r)-1] != "files" { + t.Fatal() + } +} diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 3346534ab..aa296c0f6 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -979,3 +979,59 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustH return storeChannel } + +func (s SqlPostStore) ComplianceExport(startTime int64, endTime int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + query := + `SELECT + Teams.Name AS TeamName, + Teams.DisplayName AS TeamDisplayName, + Channels.Name AS ChannelName, + Channels.DisplayName AS ChannelDisplayName, + Users.Username AS UserUsername, + Users.Email AS UserEmail, + Users.Nickname AS UserNickname, + Posts.Id AS PostId, + Posts.CreateAt AS PostCreateAt, + Posts.UpdateAt AS PostUpdateAt, + Posts.DeleteAt AS PostDeleteAt, + Posts.RootId AS PostRootId, + Posts.ParentId AS PostParentId, + Posts.OriginalId AS PostOriginalId, + Posts.Message AS PostMessage, + Posts.Type AS PostType, + Posts.Props AS PostProps, + Posts.Hashtags AS PostHashtags, + Posts.Filenames AS PostFilenames + FROM + Teams, + Channels, + Users, + Posts + WHERE + Teams.Id = Channels.TeamId + AND Posts.ChannelId = Channels.Id + AND Posts.UserId = Users.Id + AND Posts.CreateAt > :StartTime + AND Posts.CreateAt <= :EndTime + ORDER BY Posts.CreateAt + LIMIT 50000` + + var cposts []*model.CompliancePost + + if _, err := s.GetReplica().Select(&cposts, query, map[string]interface{}{"StartTime": startTime, "EndTime": endTime}); err != nil { + result.Err = model.NewLocAppError("SqlPostStore.ComplianceExport", "store.sql_post.compliance_export.app_error", nil, err.Error()) + } else { + result.Data = cposts + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go index d69f7906c..512c27ee4 100644 --- a/store/sql_post_store_test.go +++ b/store/sql_post_store_test.go @@ -895,3 +895,79 @@ func TestPostCountsByDay(t *testing.T) { } } } + +func TestComplianceExport(t *testing.T) { + Setup() + + time.Sleep(100 * time.Millisecond) + + t1 := &model.Team{} + t1.DisplayName = "DisplayName" + t1.Name = "a" + model.NewId() + "b" + t1.Email = model.NewId() + "@nowhere.com" + t1.Type = model.TEAM_OPEN + t1 = Must(store.Team().Save(t1)).(*model.Team) + + u1 := &model.User{} + u1.TeamId = t1.Id + u1.Email = model.NewId() + u1.Username = model.NewId() + u1 = Must(store.User().Save(u1)).(*model.User) + + c1 := &model.Channel{} + c1.TeamId = t1.Id + c1.DisplayName = "Channel2" + c1.Name = "a" + model.NewId() + "b" + c1.Type = model.CHANNEL_OPEN + c1 = Must(store.Channel().Save(c1)).(*model.Channel) + + o1 := &model.Post{} + o1.ChannelId = c1.Id + o1.UserId = u1.Id + o1.CreateAt = model.GetMillis() + o1.Message = "a" + model.NewId() + "b" + o1 = Must(store.Post().Save(o1)).(*model.Post) + + o1a := &model.Post{} + o1a.ChannelId = c1.Id + o1a.UserId = u1.Id + o1a.CreateAt = o1.CreateAt + 10 + o1a.Message = "a" + model.NewId() + "b" + o1a = Must(store.Post().Save(o1a)).(*model.Post) + + o2 := &model.Post{} + o2.ChannelId = c1.Id + o2.UserId = u1.Id + o2.CreateAt = o1.CreateAt + 20 + o2.Message = "a" + model.NewId() + "b" + o2 = Must(store.Post().Save(o2)).(*model.Post) + + o2a := &model.Post{} + o2a.ChannelId = c1.Id + o2a.UserId = u1.Id + o2a.CreateAt = o1.CreateAt + 30 + o2a.Message = "a" + model.NewId() + "b" + o2a = Must(store.Post().Save(o2a)).(*model.Post) + + time.Sleep(100 * time.Millisecond) + + if r1 := <-store.Post().ComplianceExport(o1.CreateAt-1, o2a.CreateAt+1); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cposts := r1.Data.([]*model.CompliancePost) + t.Log(cposts) + + if len(cposts) != 4 { + t.Fatal("return wrong results length") + } + + if cposts[0].PostId != o1.Id { + t.Fatal("Wrong sort") + } + + if cposts[3].PostId != o2a.Id { + t.Fatal("Wrong sort") + } + + } +} diff --git a/store/store.go b/store/store.go index b041cfa25..7aef18203 100644 --- a/store/store.go +++ b/store/store.go @@ -105,6 +105,7 @@ type PostStore interface { AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel AnalyticsPostCountsByDay(teamId string) StoreChannel AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel + ComplianceExport(startTime int64, endTime int64) StoreChannel } type UserStore interface { -- cgit v1.2.3-1-g7c22 From 6fa6ea0af532f2b5fb57a1f22431103b1c9e6e27 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Fri, 11 Mar 2016 16:53:32 -0800 Subject: PLT-2115 adding compliance interfaces --- einterfaces/compliance.go | 20 ++++++++++++++++++++ i18n/en.json | 16 ++++++++++++++++ mattermost.go | 5 +++++ model/system.go | 9 +++++---- 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 einterfaces/compliance.go diff --git a/einterfaces/compliance.go b/einterfaces/compliance.go new file mode 100644 index 000000000..136f9a4d3 --- /dev/null +++ b/einterfaces/compliance.go @@ -0,0 +1,20 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package einterfaces + +import () + +type ComplianceInterface interface { + StartComplianceJob() +} + +var theComplianceInterface ComplianceInterface + +func RegisterComplianceInterface(newInterface ComplianceInterface) { + theComplianceInterface = newInterface +} + +func GetComplianceInterface() ComplianceInterface { + return theComplianceInterface +} diff --git a/i18n/en.json b/i18n/en.json index 20d997af7..6eebb02c4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1775,6 +1775,22 @@ "id": "mattermost.security_bulletin_read.error", "translation": "Failed to read security bulletin details" }, + { + "id": "mattermost.compliance_init.info", + "translation": "Compliance running for the first time initializing to yesterday" + }, + { + "id": "mattermost.compliance_run.info", + "translation": "Compliance export started for {{.Time}}" + }, + { + "id": "mattermost.compliance_run_fail.error", + "translation": "Compliance export failed for {{.Time}} with err='{{.Error}}'" + }, + { + "id": "mattermost.compliance_run_finish.info", + "translation": "Compliance export finished writing {{.Count}} items to {{.Filename}}" + }, { "id": "mattermost.security_checks.debug", "translation": "Checking for security update from Mattermost" diff --git a/mattermost.go b/mattermost.go index 45ffcc88f..21620c539 100644 --- a/mattermost.go +++ b/mattermost.go @@ -19,6 +19,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/api" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/manualtesting" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" @@ -91,6 +92,10 @@ func main() { setDiagnosticId() runSecurityAndDiagnosticsJobAndForget() + if einterfaces.GetComplianceInterface() != nil { + einterfaces.GetComplianceInterface().StartComplianceJob() + } + // wait for kill signal before attempting to gracefully shutdown // the running service c := make(chan os.Signal) diff --git a/model/system.go b/model/system.go index b387749f6..68d542c15 100644 --- a/model/system.go +++ b/model/system.go @@ -9,10 +9,11 @@ import ( ) const ( - SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" - SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" - SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" - SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId" + SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" + SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" + SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" + SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId" + SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime" ) type System struct { -- cgit v1.2.3-1-g7c22 From 013f27ff1a5b502560864ebba43bd2ba6c8020ab Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Fri, 11 Mar 2016 16:58:01 -0800 Subject: Updating interface --- einterfaces/compliance.go | 2 +- mattermost.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/einterfaces/compliance.go b/einterfaces/compliance.go index 136f9a4d3..522a949db 100644 --- a/einterfaces/compliance.go +++ b/einterfaces/compliance.go @@ -6,7 +6,7 @@ package einterfaces import () type ComplianceInterface interface { - StartComplianceJob() + StartComplianceDailyJob() } var theComplianceInterface ComplianceInterface diff --git a/mattermost.go b/mattermost.go index 21620c539..de97d36a2 100644 --- a/mattermost.go +++ b/mattermost.go @@ -93,7 +93,7 @@ func main() { runSecurityAndDiagnosticsJobAndForget() if einterfaces.GetComplianceInterface() != nil { - einterfaces.GetComplianceInterface().StartComplianceJob() + einterfaces.GetComplianceInterface().StartComplianceDailyJob() } // wait for kill signal before attempting to gracefully shutdown -- cgit v1.2.3-1-g7c22 From 397ebec88c2db3569efd77238bf877e976492d34 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 14 Mar 2016 10:22:36 -0700 Subject: PLT-2115 adding compliance --- einterfaces/compliance.go | 5 ++++- i18n/en.json | 17 +++++++++-------- store/sql_post_store.go | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/einterfaces/compliance.go b/einterfaces/compliance.go index 522a949db..cd43152da 100644 --- a/einterfaces/compliance.go +++ b/einterfaces/compliance.go @@ -3,10 +3,13 @@ package einterfaces -import () +import ( + "github.com/mattermost/platform/model" +) type ComplianceInterface interface { StartComplianceDailyJob() + RunComplianceJob(jobName string, dir string, filename string, startTime int64, endTime int64) *model.AppError } var theComplianceInterface ComplianceInterface diff --git a/i18n/en.json b/i18n/en.json index 6eebb02c4..57d5179b6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1776,21 +1776,22 @@ "translation": "Failed to read security bulletin details" }, { - "id": "mattermost.compliance_init.info", - "translation": "Compliance running for the first time initializing to yesterday" + "id": "ent.compliance.run_started.info", + "translation": "Compliance export started for job '{{.JobName}}' at '{{.FilePath}}'" }, { - "id": "mattermost.compliance_run.info", - "translation": "Compliance export started for {{.Time}}" + "id": "ent.compliance.run_failed.error", + "translation": "Compliance export failed for job '{{.JobName}}' at '{{.FilePath}}'" }, { - "id": "mattermost.compliance_run_fail.error", - "translation": "Compliance export failed for {{.Time}} with err='{{.Error}}'" + "id": "ent.compliance.run_limit.warning", + "translation": "Compliance export warning for job '{{.JobName}}' too many rows returned truncating to 30,000 at '{{.FilePath}}'" }, { - "id": "mattermost.compliance_run_finish.info", - "translation": "Compliance export finished writing {{.Count}} items to {{.Filename}}" + "id": "ent.compliance.run_finished.info", + "translation": "Compliance export finished for job '{{.JobName}}' exported {{.Count}} records to '{{.FilePath}}'" }, + { "id": "mattermost.security_checks.debug", "translation": "Checking for security update from Mattermost" diff --git a/store/sql_post_store.go b/store/sql_post_store.go index aa296c0f6..198347ff2 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -1019,7 +1019,7 @@ func (s SqlPostStore) ComplianceExport(startTime int64, endTime int64) StoreChan AND Posts.CreateAt > :StartTime AND Posts.CreateAt <= :EndTime ORDER BY Posts.CreateAt - LIMIT 50000` + LIMIT 30000` var cposts []*model.CompliancePost -- cgit v1.2.3-1-g7c22 From 36b17bf99ddd35c0c223722f8b6f4f1c71b2235e Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Mon, 14 Mar 2016 16:07:58 -0700 Subject: PLT-2115 Adding compliance --- api/admin.go | 112 +++++- api/admin_test.go | 3 +- api/license.go | 20 ++ config/config.json | 34 +- einterfaces/compliance.go | 2 +- i18n/en.json | 37 +- mattermost.go | 22 +- model/client.go | 36 ++ model/compliance.go | 132 +++++++ model/compliance_post.go | 18 +- model/compliance_test.go | 19 + model/config.go | 46 ++- model/license.go | 12 +- store/sql_audit_store.go | 15 +- store/sql_compliance_store.go | 234 +++++++++++++ store/sql_compliance_store_test.go | 210 +++++++++++ store/sql_post_store.go | 56 --- store/sql_post_store_test.go | 76 ---- store/sql_store.go | 8 + store/store.go | 10 +- utils/config.go | 2 + utils/html.go | 11 +- utils/i18n.go | 6 +- utils/license.go | 1 + .../components/admin_console/admin_controller.jsx | 3 + .../components/admin_console/admin_sidebar.jsx | 17 + web/react/components/admin_console/audits.jsx | 55 +-- .../admin_console/compliance_reports.jsx | 384 +++++++++++++++++++++ .../admin_console/compliance_settings.jsx | 271 +++++++++++++++ web/react/components/audit_table.jsx | 9 +- web/react/stores/admin_store.jsx | 30 ++ web/react/utils/async_client.jsx | 26 ++ web/react/utils/client.jsx | 29 ++ web/react/utils/constants.jsx | 1 + web/sass-files/sass/partials/_admin-console.scss | 20 ++ web/static/i18n/en.json | 35 ++ 36 files changed, 1791 insertions(+), 211 deletions(-) create mode 100644 model/compliance.go create mode 100644 model/compliance_test.go create mode 100644 store/sql_compliance_store.go create mode 100644 store/sql_compliance_store_test.go create mode 100644 web/react/components/admin_console/compliance_reports.jsx create mode 100644 web/react/components/admin_console/compliance_settings.jsx diff --git a/api/admin.go b/api/admin.go index feb70aae3..9de9f5dd8 100644 --- a/api/admin.go +++ b/api/admin.go @@ -5,15 +5,18 @@ package api import ( "bufio" + "io/ioutil" "net/http" "os" + "strconv" "strings" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" - + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" + "github.com/mssola/user_agent" ) func InitAdmin(r *mux.Router) { @@ -27,8 +30,11 @@ func InitAdmin(r *mux.Router) { sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST") sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET") sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST") - sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET") - sr.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET") + sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiUserRequired(getAnalytics)).Methods("GET") + sr.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiUserRequired(getAnalytics)).Methods("GET") + sr.Handle("/save_compliance_report", ApiUserRequired(saveComplianceReport)).Methods("POST") + sr.Handle("/compliance_reports", ApiUserRequired(getComplianceReports)).Methods("GET") + sr.Handle("/download_compliance_report/{id:[A-Za-z0-9]+}", ApiUserRequired(downloadComplianceReport)).Methods("GET") } func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { @@ -142,6 +148,8 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) { return } + c.LogAudit("") + utils.SaveConfig(utils.CfgFileName, cfg) utils.LoadConfig(utils.CfgFileName) json := utils.Cfg.ToJson() @@ -174,6 +182,104 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(m))) } +func getComplianceReports(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.HasSystemAdminPermissions("getComplianceReports") { + return + } + + if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance { + c.Err = model.NewLocAppError("getComplianceReports", "ent.compliance.licence_disable.app_error", nil, "") + return + } + + if result := <-Srv.Store.Compliance().GetAll(); result.Err != nil { + c.Err = result.Err + return + } else { + crs := result.Data.(model.Compliances) + w.Write([]byte(crs.ToJson())) + } +} + +func saveComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.HasSystemAdminPermissions("getComplianceReports") { + return + } + + if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil { + c.Err = model.NewLocAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "") + return + } + + job := model.ComplianceFromJson(r.Body) + if job == nil { + c.SetInvalidParam("saveComplianceReport", "compliance") + return + } + + job.UserId = c.Session.UserId + job.Type = model.COMPLIANCE_TYPE_ADHOC + + if result := <-Srv.Store.Compliance().Save(job); result.Err != nil { + c.Err = result.Err + return + } else { + job = result.Data.(*model.Compliance) + go einterfaces.GetComplianceInterface().RunComplianceJob(job) + } + + w.Write([]byte(job.ToJson())) +} + +func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.HasSystemAdminPermissions("downloadComplianceReport") { + return + } + + if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil { + c.Err = model.NewLocAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "") + return + } + + params := mux.Vars(r) + + id := params["id"] + if len(id) != 26 { + c.SetInvalidParam("downloadComplianceReport", "id") + return + } + + if result := <-Srv.Store.Compliance().Get(id); result.Err != nil { + c.Err = result.Err + return + } else { + job := result.Data.(*model.Compliance) + c.LogAudit("downloaded " + job.JobName()) + + if f, err := ioutil.ReadFile(*utils.Cfg.ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip"); err != nil { + c.Err = model.NewLocAppError("readFile", "api.file.read_file.reading_local.app_error", nil, err.Error()) + return + } else { + w.Header().Set("Cache-Control", "max-age=2592000, public") + w.Header().Set("Content-Length", strconv.Itoa(len(f))) + w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer + + // attach extra headers to trigger a download on IE, Edge, and Safari + ua := user_agent.New(r.UserAgent()) + bname, _ := ua.Browser() + + w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"") + + if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" { + // trim off anything before the final / so we just get the file's name + w.Header().Set("Content-Type", "application/octet-stream") + } + + w.Write(f) + } + } +} + func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { if !c.HasSystemAdminPermissions("getAnalytics") { return diff --git a/api/admin_test.go b/api/admin_test.go index bdea0bc5b..67bc1d38b 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -4,11 +4,10 @@ package api import ( - "testing" - "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + "testing" ) func TestGetLogs(t *testing.T) { diff --git a/api/license.go b/api/license.go index 542b45e26..ed0771d17 100644 --- a/api/license.go +++ b/api/license.go @@ -23,6 +23,26 @@ func InitLicense(r *mux.Router) { sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET") } +func LoadLicense() { + licenseId := "" + if result := <-Srv.Store.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + licenseId = props[model.SYSTEM_ACTIVE_LICENSE_ID] + } + + if len(licenseId) != 26 { + l4g.Warn(utils.T("mattermost.load_license.find.warn")) + return + } + + if result := <-Srv.Store.License().Get(licenseId); result.Err == nil { + record := result.Data.(*model.LicenseRecord) + utils.LoadLicense([]byte(record.Bytes)) + } else { + l4g.Warn(utils.T("mattermost.load_license.find.warn")) + } +} + func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("attempt") err := r.ParseMultipartForm(model.MAX_FILE_SIZE) diff --git a/config/config.json b/config/config.json index b211b16d3..5ed5d61bc 100644 --- a/config/config.json +++ b/config/config.json @@ -19,7 +19,9 @@ "SessionLengthWebInDays": 30, "SessionLengthMobileInDays": 30, "SessionLengthSSOInDays": 30, - "SessionCacheInMinutes": 10 + "SessionCacheInMinutes": 10, + "WebsocketSecurePort": 443, + "WebsocketPort": 80 }, "TeamSettings": { "SiteName": "Mattermost", @@ -113,5 +115,33 @@ "AuthEndpoint": "", "TokenEndpoint": "", "UserApiEndpoint": "" + }, + "GoogleSettings": { + "Enable": false, + "Secret": "", + "Id": "", + "Scope": "", + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserApiEndpoint": "" + }, + "LdapSettings": { + "Enable": false, + "LdapServer": "", + "LdapPort": 389, + "BaseDN": "", + "BindUsername": "", + "BindPassword": "", + "FirstNameAttribute": "", + "LastNameAttribute": "", + "EmailAttribute": "", + "UsernameAttribute": "", + "IdAttribute": "", + "QueryTimeout": 60 + }, + "ComplianceSettings": { + "Enable": true, + "Directory": "./data/", + "EnableDaily": false } -} +} \ No newline at end of file diff --git a/einterfaces/compliance.go b/einterfaces/compliance.go index cd43152da..2e72c67d3 100644 --- a/einterfaces/compliance.go +++ b/einterfaces/compliance.go @@ -9,7 +9,7 @@ import ( type ComplianceInterface interface { StartComplianceDailyJob() - RunComplianceJob(jobName string, dir string, filename string, startTime int64, endTime int64) *model.AppError + RunComplianceJob(job *model.Compliance) *model.AppError } var theComplianceInterface ComplianceInterface diff --git a/i18n/en.json b/i18n/en.json index e42ade162..fb4cf50b4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1811,7 +1811,10 @@ "id": "ent.compliance.run_finished.info", "translation": "Compliance export finished for job '{{.JobName}}' exported {{.Count}} records to '{{.FilePath}}'" }, - + { + "id": "ent.compliance.licence_disable.app_error", + "translation": "Compliance functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license." + }, { "id": "mattermost.security_checks.debug", "translation": "Checking for security update from Mattermost" @@ -2232,6 +2235,30 @@ "id": "model.post.is_valid.user_id.app_error", "translation": "Invalid user id" }, + { + "id": "model.compliance.is_valid.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.compliance.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.compliance.is_valid.desc.app_error", + "translation": "Invalid description" + }, + { + "id": "model.compliance.is_valid.start_at.app_error", + "translation": "From must be a valid time" + }, + { + "id": "model.compliance.is_valid.end_at.app_error", + "translation": "To must be a valid time" + }, + { + "id": "model.compliance.is_valid.start_end_at.app_error", + "translation": "To must be greater than From" + }, { "id": "model.preference.is_valid.category.app_error", "translation": "Invalid category" @@ -2508,6 +2535,14 @@ "id": "store.sql_audit.save.saving.app_error", "translation": "We encountered an error saving the audit" }, + { + "id": "store.sql_compliance.save.saving.app_error", + "translation": "We encountered an error saving the compliance report" + }, + { + "id": "store.sql_compliance.get.finding.app_error", + "translation": "We encountered an error retrieving the compliance reports" + }, { "id": "store.sql_channel.analytics_type_count.app_error", "translation": "We couldn't get channel type counts" diff --git a/mattermost.go b/mattermost.go index de97d36a2..c555862e9 100644 --- a/mattermost.go +++ b/mattermost.go @@ -70,7 +70,7 @@ func main() { web.InitWeb() if model.BuildEnterpriseReady == "true" { - loadLicense() + api.LoadLicense() } if !utils.IsLicensed && len(utils.Cfg.SqlSettings.DataSourceReplicas) > 1 { @@ -106,26 +106,6 @@ func main() { } } -func loadLicense() { - licenseId := "" - if result := <-api.Srv.Store.System().Get(); result.Err == nil { - props := result.Data.(model.StringMap) - licenseId = props[model.SYSTEM_ACTIVE_LICENSE_ID] - } - - if len(licenseId) != 26 { - l4g.Warn(utils.T("mattermost.load_license.find.warn")) - return - } - - if result := <-api.Srv.Store.License().Get(licenseId); result.Err == nil { - record := result.Data.(*model.LicenseRecord) - utils.LoadLicense([]byte(record.Bytes)) - } else { - l4g.Warn(utils.T("mattermost.load_license.find.warn")) - } -} - func setDiagnosticId() { if result := <-api.Srv.Store.System().Get(); result.Err == nil { props := result.Data.(model.StringMap) diff --git a/model/client.go b/model/client.go index 3adcb980d..f5c8ad641 100644 --- a/model/client.go +++ b/model/client.go @@ -471,6 +471,42 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) { } } +func (c *Client) GetComplianceReports() (*Result, *AppError) { + if r, err := c.DoApiGet("/admin/compliance_reports", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), CompliancesFromJson(r.Body)}, nil + } +} + +func (c *Client) SaveComplianceReport(job *Compliance) (*Result, *AppError) { + if r, err := c.DoApiPost("/admin/save_compliance_report", job.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ComplianceFromJson(r.Body)}, nil + } +} + +func (c *Client) DownloadComplianceReport(id string) (*Result, *AppError) { + var rq *http.Request + rq, _ = http.NewRequest("GET", c.ApiUrl+"/admin/download_compliance_report/"+id, nil) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewLocAppError("/admin/download_compliance_report", "model.client.connecting.app_error", nil, err.Error()) + } else if rp.StatusCode >= 300 { + return nil, AppErrorFromJson(rp.Body) + } else { + return &Result{rp.Header.Get(HEADER_REQUEST_ID), + rp.Header.Get(HEADER_ETAG_SERVER), rp.Body}, nil + } +} + func (c *Client) GetTeamAnalytics(teamId, name string) (*Result, *AppError) { if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil { return nil, err diff --git a/model/compliance.go b/model/compliance.go new file mode 100644 index 000000000..4a96a597a --- /dev/null +++ b/model/compliance.go @@ -0,0 +1,132 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "strings" +) + +const ( + COMPLIANCE_STATUS_CREATED = "created" + COMPLIANCE_STATUS_RUNNING = "running" + COMPLIANCE_STATUS_FINISHED = "finished" + COMPLIANCE_STATUS_FAILED = "failed" + COMPLIANCE_STATUS_REMOVED = "removed" + + COMPLIANCE_TYPE_DAILY = "daily" + COMPLIANCE_TYPE_ADHOC = "adhoc" +) + +type Compliance struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UserId string `json:"user_id"` + Status string `json:"status"` + Count int `json:"count"` + Desc string `json:"desc"` + Type string `json:"type"` + StartAt int64 `json:"start_at"` + EndAt int64 `json:"end_at"` + Keywords string `json:"keywords"` + Emails string `json:"emails"` +} + +type Compliances []Compliance + +func (o *Compliance) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func (me *Compliance) PreSave() { + if me.Id == "" { + me.Id = NewId() + } + + if me.Status == "" { + me.Status = COMPLIANCE_STATUS_CREATED + } + + me.Count = 0 + me.Emails = strings.ToLower(me.Emails) + me.Keywords = strings.ToLower(me.Keywords) + + me.CreateAt = GetMillis() +} + +func (me *Compliance) JobName() string { + jobName := me.Type + if me.Type == COMPLIANCE_TYPE_DAILY { + jobName += "-" + me.Desc + } + + jobName += "-" + me.Id + + return jobName +} + +func (me *Compliance) IsValid() *AppError { + + if len(me.Id) != 26 { + return NewLocAppError("Compliance.IsValid", "model.compliance.is_valid.id.app_error", nil, "") + } + + if me.CreateAt == 0 { + return NewLocAppError("Compliance.IsValid", "model.compliance.is_valid.create_at.app_error", nil, "") + } + + if len(me.Desc) > 512 || len(me.Desc) == 0 { + return NewLocAppError("Compliance.IsValid", "model.compliance.is_valid.desc.app_error", nil, "") + } + + if me.StartAt == 0 { + return NewLocAppError("Compliance.IsValid", "model.compliance.is_valid.start_at.app_error", nil, "") + } + + if me.EndAt == 0 { + return NewLocAppError("Compliance.IsValid", "model.compliance.is_valid.end_at.app_error", nil, "") + } + + if me.EndAt <= me.StartAt { + return NewLocAppError("Compliance.IsValid", "model.compliance.is_valid.start_end_at.app_error", nil, "") + } + + return nil +} + +func ComplianceFromJson(data io.Reader) *Compliance { + decoder := json.NewDecoder(data) + var o Compliance + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o Compliances) ToJson() string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func CompliancesFromJson(data io.Reader) Compliances { + decoder := json.NewDecoder(data) + var o Compliances + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} diff --git a/model/compliance_post.go b/model/compliance_post.go index 636be8f17..ce26a3660 100644 --- a/model/compliance_post.go +++ b/model/compliance_post.go @@ -65,6 +65,17 @@ func CompliancePostHeader() []string { } func (me *CompliancePost) Row() []string { + + postDeleteAt := "" + if me.PostDeleteAt > 0 { + postDeleteAt = time.Unix(0, me.PostDeleteAt*int64(1000*1000)).Format(time.RFC3339) + } + + postUpdateAt := "" + if me.PostUpdateAt != me.PostCreateAt { + postUpdateAt = time.Unix(0, me.PostUpdateAt*int64(1000*1000)).Format(time.RFC3339) + } + return []string{ me.TeamName, me.TeamDisplayName, @@ -77,9 +88,10 @@ func (me *CompliancePost) Row() []string { me.UserNickname, me.PostId, - time.Unix(0, me.PostCreateAt*1000).Format(time.RFC3339), - time.Unix(0, me.PostUpdateAt*1000).Format(time.RFC3339), - time.Unix(0, me.PostDeleteAt*1000).Format(time.RFC3339), + time.Unix(0, me.PostCreateAt*int64(1000*1000)).Format(time.RFC3339), + postUpdateAt, + postDeleteAt, + me.PostRootId, me.PostParentId, me.PostOriginalId, diff --git a/model/compliance_test.go b/model/compliance_test.go new file mode 100644 index 000000000..6acc5a882 --- /dev/null +++ b/model/compliance_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestCompliance(t *testing.T) { + o := Compliance{Desc: "test", CreateAt: GetMillis()} + json := o.ToJson() + result := ComplianceFromJson(strings.NewReader(json)) + + if o.Desc != result.Desc { + t.Fatal("JobName do not match") + } +} diff --git a/model/config.go b/model/config.go index 82c51224e..8f5865f28 100644 --- a/model/config.go +++ b/model/config.go @@ -170,19 +170,26 @@ type LdapSettings struct { QueryTimeout *int } +type ComplianceSettings struct { + Enable *bool + Directory *string + EnableDaily *bool +} + type Config struct { - ServiceSettings ServiceSettings - TeamSettings TeamSettings - SqlSettings SqlSettings - LogSettings LogSettings - FileSettings FileSettings - EmailSettings EmailSettings - RateLimitSettings RateLimitSettings - PrivacySettings PrivacySettings - SupportSettings SupportSettings - GitLabSettings SSOSettings - GoogleSettings SSOSettings - LdapSettings LdapSettings + ServiceSettings ServiceSettings + TeamSettings TeamSettings + SqlSettings SqlSettings + LogSettings LogSettings + FileSettings FileSettings + EmailSettings EmailSettings + RateLimitSettings RateLimitSettings + PrivacySettings PrivacySettings + SupportSettings SupportSettings + GitLabSettings SSOSettings + GoogleSettings SSOSettings + LdapSettings LdapSettings + ComplianceSettings ComplianceSettings } func (o *Config) ToJson() string { @@ -383,6 +390,21 @@ func (o *Config) SetDefaults() { o.ServiceSettings.AllowCorsFrom = new(string) *o.ServiceSettings.AllowCorsFrom = "" } + + if o.ComplianceSettings.Enable == nil { + o.ComplianceSettings.Enable = new(bool) + *o.ComplianceSettings.Enable = false + } + + if o.ComplianceSettings.Directory == nil { + o.ComplianceSettings.Directory = new(string) + *o.ComplianceSettings.Directory = "./data/" + } + + if o.ComplianceSettings.EnableDaily == nil { + o.ComplianceSettings.EnableDaily = new(bool) + *o.ComplianceSettings.EnableDaily = false + } } func (o *Config) IsValid() *AppError { diff --git a/model/license.go b/model/license.go index ea66fef0d..8461c9f76 100644 --- a/model/license.go +++ b/model/license.go @@ -32,9 +32,10 @@ type Customer struct { } type Features struct { - Users *int `json:"users"` - LDAP *bool `json:"ldap"` - GoogleSSO *bool `json:"google_sso"` + Users *int `json:"users"` + LDAP *bool `json:"ldap"` + GoogleSSO *bool `json:"google_sso"` + Compliance *bool `json:"compliance"` } func (f *Features) SetDefaults() { @@ -52,6 +53,11 @@ func (f *Features) SetDefaults() { f.GoogleSSO = new(bool) *f.GoogleSSO = true } + + if f.Compliance == nil { + f.Compliance = new(bool) + *f.Compliance = true + } } func (l *License) IsExpired() bool { diff --git a/store/sql_audit_store.go b/store/sql_audit_store.go index dbcb9a616..7609ebc25 100644 --- a/store/sql_audit_store.go +++ b/store/sql_audit_store.go @@ -18,8 +18,8 @@ func NewSqlAuditStore(sqlStore *SqlStore) AuditStore { table := db.AddTableWithName(model.Audit{}, "Audits").SetKeys(false, "Id") table.ColMap("Id").SetMaxSize(26) table.ColMap("UserId").SetMaxSize(26) - table.ColMap("Action").SetMaxSize(64) - table.ColMap("ExtraInfo").SetMaxSize(128) + table.ColMap("Action").SetMaxSize(512) + table.ColMap("ExtraInfo").SetMaxSize(1024) table.ColMap("IpAddress").SetMaxSize(64) table.ColMap("SessionId").SetMaxSize(26) } @@ -28,6 +28,17 @@ func NewSqlAuditStore(sqlStore *SqlStore) AuditStore { } func (s SqlAuditStore) UpgradeSchemaIfNeeded() { + // ADDED for 2.2 REMOVE for 2.6 + extraLength := s.GetMaxLengthOfColumnIfExists("Audits", "ExtraInfo") + if len(extraLength) > 0 && extraLength != "1024" { + s.AlterColumnTypeIfExists("Audits", "ExtraInfo", "VARCHAR(1024)", "VARCHAR(1024)") + } + + // ADDED for 2.2 REMOVE for 2.6 + actionLength := s.GetMaxLengthOfColumnIfExists("Audits", "Action") + if len(actionLength) > 0 && actionLength != "512" { + s.AlterColumnTypeIfExists("Audits", "Action", "VARCHAR(512)", "VARCHAR(512)") + } } func (s SqlAuditStore) CreateIndexesIfNotExists() { diff --git a/store/sql_compliance_store.go b/store/sql_compliance_store.go new file mode 100644 index 000000000..57872aef4 --- /dev/null +++ b/store/sql_compliance_store.go @@ -0,0 +1,234 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "strconv" + "strings" +) + +type SqlComplianceStore struct { + *SqlStore +} + +func NewSqlComplianceStore(sqlStore *SqlStore) ComplianceStore { + s := &SqlComplianceStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Compliance{}, "Compliances").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("UserId").SetMaxSize(26) + table.ColMap("Status").SetMaxSize(64) + table.ColMap("Desc").SetMaxSize(512) + table.ColMap("Type").SetMaxSize(64) + table.ColMap("Keywords").SetMaxSize(512) + table.ColMap("Emails").SetMaxSize(1024) + } + + return s +} + +func (s SqlComplianceStore) UpgradeSchemaIfNeeded() { +} + +func (s SqlComplianceStore) CreateIndexesIfNotExists() { +} + +func (s SqlComplianceStore) Save(compliance *model.Compliance) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + compliance.PreSave() + if result.Err = compliance.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(compliance); err != nil { + result.Err = model.NewLocAppError("SqlComplianceStore.Save", "store.sql_compliance.save.saving.app_error", nil, err.Error()) + } else { + result.Data = compliance + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlComplianceStore) Update(compliance *model.Compliance) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if result.Err = compliance.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if _, err := us.GetMaster().Update(compliance); err != nil { + result.Err = model.NewLocAppError("SqlComplianceStore.Update", "store.sql_compliance.save.saving.app_error", nil, err.Error()) + } else { + result.Data = compliance + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlComplianceStore) GetAll() StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + query := "SELECT * FROM Compliances ORDER BY CreateAt DESC" + + var compliances model.Compliances + if _, err := s.GetReplica().Select(&compliances, query); err != nil { + result.Err = model.NewLocAppError("SqlComplianceStore.Get", "store.sql_compliance.get.finding.app_error", nil, err.Error()) + } else { + result.Data = compliances + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (us SqlComplianceStore) Get(id string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if obj, err := us.GetReplica().Get(model.Compliance{}, id); err != nil { + result.Err = model.NewLocAppError("SqlComplianceStore.Get", "store.sql_compliance.get.finding.app_error", nil, err.Error()) + } else if obj == nil { + result.Err = model.NewLocAppError("SqlComplianceStore.Get", "store.sql_compliance.get.finding.app_error", nil, err.Error()) + } else { + result.Data = obj.(*model.Compliance) + } + + storeChannel <- result + close(storeChannel) + + }() + + return storeChannel +} + +func (s SqlComplianceStore) ComplianceExport(job *model.Compliance) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + props := map[string]interface{}{"StartTime": job.StartAt, "EndTime": job.EndAt} + + keywordQuery := "" + keywords := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Keywords, ",", " ", -1)))) + if len(keywords) > 0 { + + keywordQuery = "AND (" + + for index, keyword := range keywords { + if index >= 1 { + keywordQuery += " OR LOWER(Posts.Message) LIKE :Keyword" + strconv.Itoa(index) + } else { + keywordQuery += "LOWER(Posts.Message) LIKE :Keyword" + strconv.Itoa(index) + } + + props["Keyword"+strconv.Itoa(index)] = "%" + keyword + "%" + } + + keywordQuery += ")" + } + + emailQuery := "" + emails := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(job.Emails, ",", " ", -1)))) + if len(emails) > 0 { + + emailQuery = "AND (" + + for index, email := range emails { + if index >= 1 { + emailQuery += " OR Users.Email = :Email" + strconv.Itoa(index) + } else { + emailQuery += "Users.Email = :Email" + strconv.Itoa(index) + } + + props["Email"+strconv.Itoa(index)] = email + } + + emailQuery += ")" + } + + query := + `SELECT + Teams.Name AS TeamName, + Teams.DisplayName AS TeamDisplayName, + Channels.Name AS ChannelName, + Channels.DisplayName AS ChannelDisplayName, + Users.Username AS UserUsername, + Users.Email AS UserEmail, + Users.Nickname AS UserNickname, + Posts.Id AS PostId, + Posts.CreateAt AS PostCreateAt, + Posts.UpdateAt AS PostUpdateAt, + Posts.DeleteAt AS PostDeleteAt, + Posts.RootId AS PostRootId, + Posts.ParentId AS PostParentId, + Posts.OriginalId AS PostOriginalId, + Posts.Message AS PostMessage, + Posts.Type AS PostType, + Posts.Props AS PostProps, + Posts.Hashtags AS PostHashtags, + Posts.Filenames AS PostFilenames + FROM + Teams, + Channels, + Users, + Posts + WHERE + Teams.Id = Channels.TeamId + AND Posts.ChannelId = Channels.Id + AND Posts.UserId = Users.Id + AND Posts.CreateAt > :StartTime + AND Posts.CreateAt <= :EndTime + ` + emailQuery + ` + ` + keywordQuery + ` + ORDER BY Posts.CreateAt + LIMIT 30000` + + var cposts []*model.CompliancePost + + if _, err := s.GetReplica().Select(&cposts, query, props); err != nil { + result.Err = model.NewLocAppError("SqlPostStore.ComplianceExport", "store.sql_post.compliance_export.app_error", nil, err.Error()) + } else { + result.Data = cposts + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_compliance_store_test.go b/store/sql_compliance_store_test.go new file mode 100644 index 000000000..2f3ef3569 --- /dev/null +++ b/store/sql_compliance_store_test.go @@ -0,0 +1,210 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" + "time" +) + +func TestSqlComplianceStore(t *testing.T) { + Setup() + + compliance1 := &model.Compliance{Desc: "Desc", UserId: model.NewId(), Status: "TestStatus1", StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1} + Must(store.Compliance().Save(compliance1)) + time.Sleep(100 * time.Millisecond) + + compliance2 := &model.Compliance{Desc: "Desc", UserId: model.NewId(), Status: "TestStatus2", StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1} + Must(store.Compliance().Save(compliance2)) + time.Sleep(100 * time.Millisecond) + + c := store.Compliance().GetAll() + result := <-c + compliances := result.Data.(model.Compliances) + + if compliances[0].Status != "TestStatus2" && compliance2.Id != compliances[0].Id { + t.Fatal() + } + + compliance2.Status = "TestUpdateStatus2" + Must(store.Compliance().Update(compliance2)) + + c = store.Compliance().GetAll() + result = <-c + compliances = result.Data.(model.Compliances) + + if compliances[0].Status != "TestUpdateStatus2" && compliance2.Id != compliances[0].Id { + t.Fatal() + } + + rc2 := (<-store.Compliance().Get(compliance2.Id)).Data.(*model.Compliance) + if rc2.Status != compliance2.Status { + t.Fatal() + } +} + +func TestComplianceExport(t *testing.T) { + Setup() + + time.Sleep(100 * time.Millisecond) + + t1 := &model.Team{} + t1.DisplayName = "DisplayName" + t1.Name = "a" + model.NewId() + "b" + t1.Email = model.NewId() + "@nowhere.com" + t1.Type = model.TEAM_OPEN + t1 = Must(store.Team().Save(t1)).(*model.Team) + + u1 := &model.User{} + u1.TeamId = t1.Id + u1.Email = model.NewId() + u1.Username = model.NewId() + u1 = Must(store.User().Save(u1)).(*model.User) + + u2 := &model.User{} + u2.TeamId = t1.Id + u2.Email = model.NewId() + u2.Username = model.NewId() + u2 = Must(store.User().Save(u2)).(*model.User) + + c1 := &model.Channel{} + c1.TeamId = t1.Id + c1.DisplayName = "Channel2" + c1.Name = "a" + model.NewId() + "b" + c1.Type = model.CHANNEL_OPEN + c1 = Must(store.Channel().Save(c1)).(*model.Channel) + + o1 := &model.Post{} + o1.ChannelId = c1.Id + o1.UserId = u1.Id + o1.CreateAt = model.GetMillis() + o1.Message = "a" + model.NewId() + "b" + o1 = Must(store.Post().Save(o1)).(*model.Post) + + o1a := &model.Post{} + o1a.ChannelId = c1.Id + o1a.UserId = u1.Id + o1a.CreateAt = o1.CreateAt + 10 + o1a.Message = "a" + model.NewId() + "b" + o1a = Must(store.Post().Save(o1a)).(*model.Post) + + o2 := &model.Post{} + o2.ChannelId = c1.Id + o2.UserId = u1.Id + o2.CreateAt = o1.CreateAt + 20 + o2.Message = "a" + model.NewId() + "b" + o2 = Must(store.Post().Save(o2)).(*model.Post) + + o2a := &model.Post{} + o2a.ChannelId = c1.Id + o2a.UserId = u2.Id + o2a.CreateAt = o1.CreateAt + 30 + o2a.Message = "a" + model.NewId() + "b" + o2a = Must(store.Post().Save(o2a)).(*model.Post) + + time.Sleep(100 * time.Millisecond) + + cr1 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1} + if r1 := <-store.Compliance().ComplianceExport(cr1); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cposts := r1.Data.([]*model.CompliancePost) + + if len(cposts) != 4 { + t.Fatal("return wrong results length") + } + + if cposts[0].PostId != o1.Id { + t.Fatal("Wrong sort") + } + + if cposts[3].PostId != o2a.Id { + t.Fatal("Wrong sort") + } + } + + cr2 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email} + if r1 := <-store.Compliance().ComplianceExport(cr2); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cposts := r1.Data.([]*model.CompliancePost) + + if len(cposts) != 1 { + t.Fatal("return wrong results length") + } + + if cposts[0].PostId != o2a.Id { + t.Fatal("Wrong sort") + } + } + + cr3 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email + ", " + u1.Email} + if r1 := <-store.Compliance().ComplianceExport(cr3); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cposts := r1.Data.([]*model.CompliancePost) + + if len(cposts) != 4 { + t.Fatal("return wrong results length") + } + + if cposts[0].PostId != o1.Id { + t.Fatal("Wrong sort") + } + + if cposts[3].PostId != o2a.Id { + t.Fatal("Wrong sort") + } + } + + cr4 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Keywords: o2a.Message} + if r1 := <-store.Compliance().ComplianceExport(cr4); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cposts := r1.Data.([]*model.CompliancePost) + + if len(cposts) != 1 { + t.Fatal("return wrong results length") + } + + if cposts[0].PostId != o2a.Id { + t.Fatal("Wrong sort") + } + } + + cr5 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Keywords: o2a.Message + " " + o1.Message} + if r1 := <-store.Compliance().ComplianceExport(cr5); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cposts := r1.Data.([]*model.CompliancePost) + + if len(cposts) != 2 { + t.Fatal("return wrong results length") + } + + if cposts[0].PostId != o1.Id { + t.Fatal("Wrong sort") + } + } + + cr6 := &model.Compliance{Desc: "test" + model.NewId(), StartAt: o1.CreateAt - 1, EndAt: o2a.CreateAt + 1, Emails: u2.Email + ", " + u1.Email, Keywords: o2a.Message + " " + o1.Message} + if r1 := <-store.Compliance().ComplianceExport(cr6); r1.Err != nil { + t.Fatal(r1.Err) + } else { + cposts := r1.Data.([]*model.CompliancePost) + + if len(cposts) != 2 { + t.Fatal("return wrong results length") + } + + if cposts[0].PostId != o1.Id { + t.Fatal("Wrong sort") + } + + if cposts[1].PostId != o2a.Id { + t.Fatal("Wrong sort") + } + } +} diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 198347ff2..3346534ab 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -979,59 +979,3 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustH return storeChannel } - -func (s SqlPostStore) ComplianceExport(startTime int64, endTime int64) StoreChannel { - storeChannel := make(StoreChannel) - - go func() { - result := StoreResult{} - - query := - `SELECT - Teams.Name AS TeamName, - Teams.DisplayName AS TeamDisplayName, - Channels.Name AS ChannelName, - Channels.DisplayName AS ChannelDisplayName, - Users.Username AS UserUsername, - Users.Email AS UserEmail, - Users.Nickname AS UserNickname, - Posts.Id AS PostId, - Posts.CreateAt AS PostCreateAt, - Posts.UpdateAt AS PostUpdateAt, - Posts.DeleteAt AS PostDeleteAt, - Posts.RootId AS PostRootId, - Posts.ParentId AS PostParentId, - Posts.OriginalId AS PostOriginalId, - Posts.Message AS PostMessage, - Posts.Type AS PostType, - Posts.Props AS PostProps, - Posts.Hashtags AS PostHashtags, - Posts.Filenames AS PostFilenames - FROM - Teams, - Channels, - Users, - Posts - WHERE - Teams.Id = Channels.TeamId - AND Posts.ChannelId = Channels.Id - AND Posts.UserId = Users.Id - AND Posts.CreateAt > :StartTime - AND Posts.CreateAt <= :EndTime - ORDER BY Posts.CreateAt - LIMIT 30000` - - var cposts []*model.CompliancePost - - if _, err := s.GetReplica().Select(&cposts, query, map[string]interface{}{"StartTime": startTime, "EndTime": endTime}); err != nil { - result.Err = model.NewLocAppError("SqlPostStore.ComplianceExport", "store.sql_post.compliance_export.app_error", nil, err.Error()) - } else { - result.Data = cposts - } - - storeChannel <- result - close(storeChannel) - }() - - return storeChannel -} diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go index 512c27ee4..d69f7906c 100644 --- a/store/sql_post_store_test.go +++ b/store/sql_post_store_test.go @@ -895,79 +895,3 @@ func TestPostCountsByDay(t *testing.T) { } } } - -func TestComplianceExport(t *testing.T) { - Setup() - - time.Sleep(100 * time.Millisecond) - - t1 := &model.Team{} - t1.DisplayName = "DisplayName" - t1.Name = "a" + model.NewId() + "b" - t1.Email = model.NewId() + "@nowhere.com" - t1.Type = model.TEAM_OPEN - t1 = Must(store.Team().Save(t1)).(*model.Team) - - u1 := &model.User{} - u1.TeamId = t1.Id - u1.Email = model.NewId() - u1.Username = model.NewId() - u1 = Must(store.User().Save(u1)).(*model.User) - - c1 := &model.Channel{} - c1.TeamId = t1.Id - c1.DisplayName = "Channel2" - c1.Name = "a" + model.NewId() + "b" - c1.Type = model.CHANNEL_OPEN - c1 = Must(store.Channel().Save(c1)).(*model.Channel) - - o1 := &model.Post{} - o1.ChannelId = c1.Id - o1.UserId = u1.Id - o1.CreateAt = model.GetMillis() - o1.Message = "a" + model.NewId() + "b" - o1 = Must(store.Post().Save(o1)).(*model.Post) - - o1a := &model.Post{} - o1a.ChannelId = c1.Id - o1a.UserId = u1.Id - o1a.CreateAt = o1.CreateAt + 10 - o1a.Message = "a" + model.NewId() + "b" - o1a = Must(store.Post().Save(o1a)).(*model.Post) - - o2 := &model.Post{} - o2.ChannelId = c1.Id - o2.UserId = u1.Id - o2.CreateAt = o1.CreateAt + 20 - o2.Message = "a" + model.NewId() + "b" - o2 = Must(store.Post().Save(o2)).(*model.Post) - - o2a := &model.Post{} - o2a.ChannelId = c1.Id - o2a.UserId = u1.Id - o2a.CreateAt = o1.CreateAt + 30 - o2a.Message = "a" + model.NewId() + "b" - o2a = Must(store.Post().Save(o2a)).(*model.Post) - - time.Sleep(100 * time.Millisecond) - - if r1 := <-store.Post().ComplianceExport(o1.CreateAt-1, o2a.CreateAt+1); r1.Err != nil { - t.Fatal(r1.Err) - } else { - cposts := r1.Data.([]*model.CompliancePost) - t.Log(cposts) - - if len(cposts) != 4 { - t.Fatal("return wrong results length") - } - - if cposts[0].PostId != o1.Id { - t.Fatal("Wrong sort") - } - - if cposts[3].PostId != o2a.Id { - t.Fatal("Wrong sort") - } - - } -} diff --git a/store/sql_store.go b/store/sql_store.go index de23f4db3..8ff5da6f7 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -43,6 +43,7 @@ type SqlStore struct { post PostStore user UserStore audit AuditStore + compliance ComplianceStore session SessionStore oauth OAuthStore system SystemStore @@ -98,6 +99,7 @@ func NewSqlStore() Store { sqlStore.post = NewSqlPostStore(sqlStore) sqlStore.user = NewSqlUserStore(sqlStore) sqlStore.audit = NewSqlAuditStore(sqlStore) + sqlStore.compliance = NewSqlComplianceStore(sqlStore) sqlStore.session = NewSqlSessionStore(sqlStore) sqlStore.oauth = NewSqlOAuthStore(sqlStore) sqlStore.system = NewSqlSystemStore(sqlStore) @@ -116,6 +118,7 @@ func NewSqlStore() Store { sqlStore.post.(*SqlPostStore).UpgradeSchemaIfNeeded() sqlStore.user.(*SqlUserStore).UpgradeSchemaIfNeeded() sqlStore.audit.(*SqlAuditStore).UpgradeSchemaIfNeeded() + sqlStore.compliance.(*SqlComplianceStore).UpgradeSchemaIfNeeded() sqlStore.session.(*SqlSessionStore).UpgradeSchemaIfNeeded() sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded() sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded() @@ -129,6 +132,7 @@ func NewSqlStore() Store { sqlStore.post.(*SqlPostStore).CreateIndexesIfNotExists() sqlStore.user.(*SqlUserStore).CreateIndexesIfNotExists() sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists() + sqlStore.compliance.(*SqlComplianceStore).CreateIndexesIfNotExists() sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists() sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists() sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists() @@ -591,6 +595,10 @@ func (ss SqlStore) Audit() AuditStore { return ss.audit } +func (ss SqlStore) Compliance() ComplianceStore { + return ss.compliance +} + func (ss SqlStore) OAuth() OAuthStore { return ss.oauth } diff --git a/store/store.go b/store/store.go index 7aef18203..94c426117 100644 --- a/store/store.go +++ b/store/store.go @@ -33,6 +33,7 @@ type Store interface { Post() PostStore User() UserStore Audit() AuditStore + Compliance() ComplianceStore Session() SessionStore OAuth() OAuthStore System() SystemStore @@ -105,7 +106,6 @@ type PostStore interface { AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel AnalyticsPostCountsByDay(teamId string) StoreChannel AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) StoreChannel - ComplianceExport(startTime int64, endTime int64) StoreChannel } type UserStore interface { @@ -152,6 +152,14 @@ type AuditStore interface { PermanentDeleteByUser(userId string) StoreChannel } +type ComplianceStore interface { + Save(compliance *model.Compliance) StoreChannel + Update(compliance *model.Compliance) StoreChannel + Get(id string) StoreChannel + GetAll() StoreChannel + ComplianceExport(compliance *model.Compliance) StoreChannel +} + type OAuthStore interface { SaveApp(app *model.OAuthApp) StoreChannel UpdateApp(app *model.OAuthApp) StoreChannel diff --git a/utils/config.go b/utils/config.go index 63906c345..9624196be 100644 --- a/utils/config.go +++ b/utils/config.go @@ -238,5 +238,7 @@ func getClientConfig(c *model.Config) map[string]string { props["AllowCorsFrom"] = *c.ServiceSettings.AllowCorsFrom + props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable) + return props } diff --git a/utils/html.go b/utils/html.go index 4203160d5..e89cb12a0 100644 --- a/utils/html.go +++ b/utils/html.go @@ -23,7 +23,16 @@ type HTMLTemplate struct { } func InitHTML() { - templatesDir := FindDir("templates") + InitHTMLWithDir("templates") +} + +func InitHTMLWithDir(dir string) { + + if htmlTemplates != nil { + return + } + + templatesDir := FindDir(dir) l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) var err error if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { diff --git a/utils/i18n.go b/utils/i18n.go index e809ae883..2503cd500 100644 --- a/utils/i18n.go +++ b/utils/i18n.go @@ -16,7 +16,11 @@ var T i18n.TranslateFunc var locales map[string]string = make(map[string]string) func InitTranslations() { - i18nDirectory := FindDir("i18n") + InitTranslationsWithDir("i18n") +} + +func InitTranslationsWithDir(dir string) { + i18nDirectory := FindDir(dir) files, _ := ioutil.ReadDir(i18nDirectory) for _, f := range files { if filepath.Ext(f.Name()) == ".json" { diff --git a/utils/license.go b/utils/license.go index b1f15ad92..1dc8bf025 100644 --- a/utils/license.go +++ b/utils/license.go @@ -115,6 +115,7 @@ func getClientLicense(l *model.License) map[string]string { props["Users"] = strconv.Itoa(*l.Features.Users) props["LDAP"] = strconv.FormatBool(*l.Features.LDAP) props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO) + props["Compliance"] = strconv.FormatBool(*l.Features.Compliance) props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10) props["StartsAt"] = strconv.FormatInt(l.StartsAt, 10) props["ExpiresAt"] = strconv.FormatInt(l.ExpiresAt, 10) diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 4c4f21f08..66b6eb71f 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -22,6 +22,7 @@ import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx'; import TeamUsersTab from './team_users.jsx'; import TeamAnalyticsTab from '../analytics/team_analytics.jsx'; import LdapSettingsTab from './ldap_settings.jsx'; +import ComplianceSettingsTab from './compliance_settings.jsx'; import LicenseSettingsTab from './license_settings.jsx'; import SystemAnalyticsTab from '../analytics/system_analytics.jsx'; @@ -156,6 +157,8 @@ export default class AdminController extends React.Component { tab = ; } else if (this.state.selected === 'ldap_settings') { tab = ; + } else if (this.state.selected === 'compliance_settings') { + tab = ; } else if (this.state.selected === 'license') { tab = ; } else if (this.state.selected === 'team_users') { diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index c2f31f569..b4288d657 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -176,6 +176,7 @@ export default class AdminSidebar extends React.Component { } let ldapSettings; + let complianceSettings; let licenseSettings; if (global.window.mm_config.BuildEnterpriseReady === 'true') { if (global.window.mm_license.IsLicensed === 'true') { @@ -193,6 +194,21 @@ export default class AdminSidebar extends React.Component { ); + + complianceSettings = ( +
  • + + + +
  • + ); } licenseSettings = ( @@ -386,6 +402,7 @@ export default class AdminSidebar extends React.Component { {ldapSettings} + {complianceSettings}
  • - + ); } return ( -
    -

    - -

    - -
    - {content} +
    + + +
    +

    + +

    + +
    + {content} +
    ); diff --git a/web/react/components/admin_console/compliance_reports.jsx b/web/react/components/admin_console/compliance_reports.jsx new file mode 100644 index 000000000..2a94b6f1d --- /dev/null +++ b/web/react/components/admin_console/compliance_reports.jsx @@ -0,0 +1,384 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from '../loading_screen.jsx'; +import * as Utils from '../../utils/utils.jsx'; +import AdminStore from '../../stores/admin_store.jsx'; +import UserStore from '../../stores/user_store.jsx'; + +import * as Client from '../../utils/client.jsx'; +import * as AsyncClient from '../../utils/async_client.jsx'; + +import {FormattedMessage, FormattedDate, FormattedTime} from 'mm-intl'; + +export default class ComplianceReports extends React.Component { + constructor(props) { + super(props); + + this.onComplianceReportsListenerChange = this.onComplianceReportsListenerChange.bind(this); + this.reload = this.reload.bind(this); + this.runReport = this.runReport.bind(this); + this.getDateTime = this.getDateTime.bind(this); + + this.state = { + reports: AdminStore.getComplianceReports(), + serverError: null + }; + } + + componentDidMount() { + AdminStore.addComplianceReportsChangeListener(this.onComplianceReportsListenerChange); + + if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.EnableCompliance !== 'true') { + return; + } + + AsyncClient.getComplianceReports(); + } + + componentWillUnmount() { + AdminStore.removeComplianceReportsChangeListener(this.onComplianceReportsListenerChange); + } + + onComplianceReportsListenerChange() { + this.setState({ + reports: AdminStore.getComplianceReports() + }); + } + + reload() { + AdminStore.saveComplianceReports(null); + this.setState({ + reports: null, + serverError: null + }); + + AsyncClient.getComplianceReports(); + } + + runReport(e) { + e.preventDefault(); + $('#run-button').button('loading'); + + var job = {}; + job.desc = ReactDOM.findDOMNode(this.refs.desc).value; + job.emails = ReactDOM.findDOMNode(this.refs.emails).value; + job.keywords = ReactDOM.findDOMNode(this.refs.keywords).value; + job.start_at = Date.parse(ReactDOM.findDOMNode(this.refs.from).value); + job.end_at = Date.parse(ReactDOM.findDOMNode(this.refs.to).value); + + Client.saveComplianceReports( + job, + () => { + ReactDOM.findDOMNode(this.refs.emails).value = ''; + ReactDOM.findDOMNode(this.refs.keywords).value = ''; + ReactDOM.findDOMNode(this.refs.desc).value = ''; + ReactDOM.findDOMNode(this.refs.from).value = ''; + ReactDOM.findDOMNode(this.refs.to).value = ''; + this.reload(); + $('#run-button').button('reset'); + }, + (err) => { + this.setState({serverError: err.message}); + $('#run-button').button('reset'); + } + ); + } + + getDateTime(millis) { + const date = new Date(millis); + return ( + + + {' - '} + + + ); + } + + render() { + var content = null; + + if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.EnableCompliance !== 'true') { + return
    ; + } + + if (this.state.reports === null) { + content = ; + } else { + var list = []; + + for (var i = 0; i < this.state.reports.length; i++) { + const report = this.state.reports[i]; + + var params = ''; + if (report.type === 'adhoc') { + params = ( + + {' '}{this.getDateTime(report.start_at)} +
    + {' '}{this.getDateTime(report.end_at)} +
    + {' '}{report.emails} +
    + {' '}{report.keywords} +
    ); + } + + var download = ''; + if (report.status === 'finished') { + download = ( +
    + + + ); + } + + var status = report.status; + if (report.status === 'finished') { + status = ( + {report.status} + ); + } + + if (report.status === 'failed') { + status = ( + {report.status} + ); + } + + var user = report.user_id; + var profile = UserStore.getProfile(report.user_id); + if (profile) { + user = profile.email; + } + + list[i] = ( + + {download} + {this.getDateTime(report.create_at)} + {status} + {report.count} + {report.type} + {report.desc} + {user} + {params} + + ); + } + + content = ( +
    + + + + + + + + + + + + + + + {list} + +
    + + + + + + + + + + + + + +
    +
    + ); + } + + let serverError = ''; + if (this.state.serverError) { + serverError = ( +
    + +
    + ); + } + + return ( +
    +

    + +

    + + + + + + + + + + + + + + +
    + + +
    + + + + + + + + + + + + + +
    + {serverError} +
    + +
    +
    + {content} +
    +
    + ); + } +} diff --git a/web/react/components/admin_console/compliance_settings.jsx b/web/react/components/admin_console/compliance_settings.jsx new file mode 100644 index 000000000..8e6ca6340 --- /dev/null +++ b/web/react/components/admin_console/compliance_settings.jsx @@ -0,0 +1,271 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from '../../utils/client.jsx'; +import * as AsyncClient from '../../utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; + +var holders = defineMessages({ + saving: { + id: 'admin.compliance.saving', + defaultMessage: 'Saving Config...' + }, + directoryExample: { + id: 'admin.compliance.directoryExample', + defaultMessage: 'Ex "./data/"' + } +}); + +class ComplianceSettings extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleEnable = this.handleEnable.bind(this); + this.handleDisable = this.handleDisable.bind(this); + + this.state = { + saveNeeded: false, + serverError: null, + enable: this.props.config.ComplianceSettings.Enable + }; + } + handleChange() { + this.setState({saveNeeded: true}); + } + handleEnable() { + this.setState({saveNeeded: true, enable: true}); + } + handleDisable() { + this.setState({saveNeeded: true, enable: false}); + } + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + const config = this.props.config; + config.ComplianceSettings.Enable = this.refs.Enable.checked; + config.ComplianceSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value; + config.ComplianceSettings.EnableDaily = this.refs.EnableDaily.checked; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + render() { + const {formatMessage} = this.props.intl; + let serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + let saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.Compliance === 'true'; + + let bannerContent; + if (!licenseEnabled) { + bannerContent = ( +
    +
    + +
    +
    + ); + } + + return ( +
    + {bannerContent} +

    + +

    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    +
    +
    + ); + } +} +ComplianceSettings.defaultProps = { +}; + +ComplianceSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(ComplianceSettings); diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx index 917093840..2465950ce 100644 --- a/web/react/components/audit_table.jsx +++ b/web/react/components/audit_table.jsx @@ -217,7 +217,12 @@ class AuditTable extends React.Component { let uContent; if (this.props.showUserId) { - uContent = {auditInfo.userId}; + var profile = UserStore.getProfile(auditInfo.userId); + if (profile) { + uContent = {profile.email}; + } else { + uContent = {auditInfo.userId}; + } } let iContent; @@ -560,6 +565,8 @@ export function formatAuditInfo(audit, formatMessage) { default: break; } + } else if (actionURL.indexOf('/admin/download_compliance_report') === 0) { + auditDesc = Utils.toTitleCase(audit.extra_info); } else { switch (actionURL) { case '/logout': diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index 9f7f6e7ff..8662723be 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -13,6 +13,7 @@ const LOG_CHANGE_EVENT = 'log_change'; const SERVER_AUDIT_CHANGE_EVENT = 'server_audit_change'; const CONFIG_CHANGE_EVENT = 'config_change'; const ALL_TEAMS_EVENT = 'all_team_change'; +const SERVER_COMPLIANCE_REPORT_CHANGE_EVENT = 'server_compliance_reports_change'; class AdminStoreClass extends EventEmitter { constructor() { @@ -22,6 +23,7 @@ class AdminStoreClass extends EventEmitter { this.audits = null; this.config = null; this.teams = null; + this.complianceReports = null; this.emitLogChange = this.emitLogChange.bind(this); this.addLogChangeListener = this.addLogChangeListener.bind(this); @@ -31,6 +33,10 @@ class AdminStoreClass extends EventEmitter { this.addAuditChangeListener = this.addAuditChangeListener.bind(this); this.removeAuditChangeListener = this.removeAuditChangeListener.bind(this); + this.emitComplianceReportsChange = this.emitComplianceReportsChange.bind(this); + this.addComplianceReportsChangeListener = this.addComplianceReportsChangeListener.bind(this); + this.removeComplianceReportsChangeListener = this.removeComplianceReportsChangeListener.bind(this); + this.emitConfigChange = this.emitConfigChange.bind(this); this.addConfigChangeListener = this.addConfigChangeListener.bind(this); this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this); @@ -64,6 +70,18 @@ class AdminStoreClass extends EventEmitter { this.removeListener(SERVER_AUDIT_CHANGE_EVENT, callback); } + emitComplianceReportsChange() { + this.emit(SERVER_COMPLIANCE_REPORT_CHANGE_EVENT); + } + + addComplianceReportsChangeListener(callback) { + this.on(SERVER_COMPLIANCE_REPORT_CHANGE_EVENT, callback); + } + + removeComplianceReportsChangeListener(callback) { + this.removeListener(SERVER_COMPLIANCE_REPORT_CHANGE_EVENT, callback); + } + emitConfigChange() { this.emit(CONFIG_CHANGE_EVENT); } @@ -104,6 +122,14 @@ class AdminStoreClass extends EventEmitter { this.audits = audits; } + getComplianceReports() { + return this.complianceReports; + } + + saveComplianceReports(complianceReports) { + this.complianceReports = complianceReports; + } + getConfig() { return this.config; } @@ -147,6 +173,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { AdminStore.saveAudits(action.audits); AdminStore.emitAuditChange(); break; + case ActionTypes.RECEIVED_SERVER_COMPLIANCE_REPORTS: + AdminStore.saveComplianceReports(action.complianceReports); + AdminStore.emitComplianceReportsChange(); + break; case ActionTypes.RECEIVED_CONFIG: AdminStore.saveConfig(action.config); AdminStore.emitConfigChange(); diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index b9770a6e9..c565e076a 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -341,6 +341,32 @@ export function getServerAudits() { ); } +export function getComplianceReports() { + if (isCallInProgress('getComplianceReports')) { + return; + } + + callTracker.getComplianceReports = utils.getTimestamp(); + client.getComplianceReports( + (data, textStatus, xhr) => { + callTracker.getComplianceReports = 0; + + if (xhr.status === 304 || !data) { + return; + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SERVER_COMPLIANCE_REPORTS, + complianceReports: data + }); + }, + (err) => { + callTracker.getComplianceReports = 0; + dispatchError(err, 'getComplianceReports'); + } + ); +} + export function getConfig() { if (isCallInProgress('getConfig')) { return; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index e00f28a14..5607a4b60 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -412,6 +412,35 @@ export function getAudits(userId, success, error) { }); } +export function getComplianceReports(success, error) { + $.ajax({ + url: '/api/v1/admin/compliance_reports', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getComplianceReports', xhr, status, err); + error(e); + } + }); +} + +export function saveComplianceReports(job, success, error) { + $.ajax({ + url: '/api/v1/admin/save_compliance_report', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(job), + success, + error: (xhr, status, err) => { + var e = handleError('saveComplianceReports', xhr, status, err); + error(e); + } + }); +} + export function getLogs(success, error) { $.ajax({ url: '/api/v1/admin/logs', diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 3de562b7b..3d9bb7317 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -47,6 +47,7 @@ export default { RECEIVED_CONFIG: null, RECEIVED_LOGS: null, RECEIVED_SERVER_AUDITS: null, + RECEIVED_SERVER_COMPLIANCE_REPORTS: null, RECEIVED_ALL_TEAMS: null, RECEIVED_LOCALE: null, diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index 76081710f..4613ff6ee 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -125,6 +125,26 @@ background-color: white; } + .compliance__panel { + overflow: scroll; + width: 100%; + height: 400px; + border: 1px solid #ddd; + margin-top: 10px; + padding: 5px; + background-color: white; + } + + .audit__panel { + overflow: scroll; + width: 100%; + height: 400px; + border: 1px solid #ddd; + margin-top: 10px; + padding: 5px; + background-color: white; + } + .app__content { &.admin { overflow: auto; diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 2a536925c..ba54fbfbd 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -191,6 +191,40 @@ "admin.ldap.uernameAttrDesc": "The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.", "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Username Attribute:", + "admin.compliance.saving": "Saving Config...", + "admin.compliance.directoryExample": "Ex \"./data/\"", + "admin.compliance.noLicense": "

    Note:

    Compliance is an enterprise feature. Your current license does not support Compliance. Click here for information and pricing on enterprise licenses.

    ", + "admin.compliance.title": "Compliance Settings", + "admin.compliance.enableTitle": "Enable Compliance:", + "admin.compliance.true": "true", + "admin.compliance.false": "false", + "admin.compliance.enableDesc": "When true, Mattermost allows compliance reporting", + "admin.compliance.directoryTitle": "Compliance Directory Location:", + "admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.", + "admin.compliance.enableDailyTitle": "Enable Daily Report:", + "admin.compliance.enableDesc": "When true, Mattermost will generate a daily compliance report.", + "admin.compliance.save": "Save", + "admin.compliance_reports.from": "From:", + "admin.compliance_reports.to": "To:", + "admin.compliance_reports.emails": "Emails:", + "admin.compliance_reports.keywords": "Keywords:", + "admin.compliance_table.download": "Download", + "admin.compliance_table.timestamp": "Timestamp", + "admin.compliance_table.status": "Status", + "admin.compliance_table.records": "Records", + "admin.compliance_table.type": "Type", + "admin.compliance_table.desc": "Description", + "admin.compliance_table.userId": "Requested By", + "admin.compliance_table.params": "Params", + "admin.compliance_reports.title": "Compliance Reports", + "admin.compliance_reports.desc": "Job Name:", + "admin.compliance_reports.desc_placeholder": "Ex \"Audit 445 for HR\"", + "admin.compliance_reports.from_placeholder": "Ex \"2016-03-11\"", + "admin.compliance_reports.to_placeholder": "Ex \"2016-03-15\"", + "admin.compliance_reports.emails_placeholder": "Ex \"bill@example.com, bob@example.com\"", + "admin.compliance_reports.keywords_placeholder": "Ex \"shorting stock\"", + "admin.compliance_reports.run": "Run", + "admin.compliance_reports.reload": "Reload", "admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, disable all Enterprise Edition features on this server. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.", "admin.license.chooseFile": "Choose File", "admin.license.edition": "Edition: ", @@ -331,6 +365,7 @@ "admin.sidebar.gitlab": "GitLab Settings", "admin.sidebar.ldap": "LDAP Settings", "admin.sidebar.license": "Edition and License", + "admin.sidebar.compliance": "Compliance Settings", "admin.sidebar.loading": "Loading", "admin.sidebar.log": "Log Settings", "admin.sidebar.logs": "Logs", -- cgit v1.2.3-1-g7c22 From ae2e80148ee80422d06a9a82569f0188800ad065 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 15 Mar 2016 18:30:41 -0700 Subject: Fixing config file --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index 5ed5d61bc..8b58e64e4 100644 --- a/config/config.json +++ b/config/config.json @@ -140,7 +140,7 @@ "QueryTimeout": 60 }, "ComplianceSettings": { - "Enable": true, + "Enable": false, "Directory": "./data/", "EnableDaily": false } -- cgit v1.2.3-1-g7c22 From e101b2cf7c172d1c4ff20e0df63917b5b8f923ed Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 15 Mar 2016 19:54:04 -0700 Subject: Fixing unit tests --- api/admin.go | 2 +- store/sql_compliance_store_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/admin.go b/api/admin.go index 9de9f5dd8..2990691a6 100644 --- a/api/admin.go +++ b/api/admin.go @@ -254,7 +254,7 @@ func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request return } else { job := result.Data.(*model.Compliance) - c.LogAudit("downloaded " + job.JobName()) + c.LogAudit("downloaded " + job.Desc) if f, err := ioutil.ReadFile(*utils.Cfg.ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip"); err != nil { c.Err = model.NewLocAppError("readFile", "api.file.read_file.reading_local.app_error", nil, err.Error()) diff --git a/store/sql_compliance_store_test.go b/store/sql_compliance_store_test.go index 2f3ef3569..1a41fa389 100644 --- a/store/sql_compliance_store_test.go +++ b/store/sql_compliance_store_test.go @@ -12,11 +12,11 @@ import ( func TestSqlComplianceStore(t *testing.T) { Setup() - compliance1 := &model.Compliance{Desc: "Desc", UserId: model.NewId(), Status: "TestStatus1", StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1} + compliance1 := &model.Compliance{Desc: "Audit for federal subpoena case #22443", UserId: model.NewId(), Status: model.COMPLIANCE_STATUS_FAILED, StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1, Type: model.COMPLIANCE_TYPE_ADHOC} Must(store.Compliance().Save(compliance1)) time.Sleep(100 * time.Millisecond) - compliance2 := &model.Compliance{Desc: "Desc", UserId: model.NewId(), Status: "TestStatus2", StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1} + compliance2 := &model.Compliance{Desc: "Audit for federal subpoena case #11458", UserId: model.NewId(), Status: model.COMPLIANCE_STATUS_RUNNING, StartAt: model.GetMillis() - 1, EndAt: model.GetMillis() + 1, Type: model.COMPLIANCE_TYPE_ADHOC} Must(store.Compliance().Save(compliance2)) time.Sleep(100 * time.Millisecond) @@ -24,18 +24,18 @@ func TestSqlComplianceStore(t *testing.T) { result := <-c compliances := result.Data.(model.Compliances) - if compliances[0].Status != "TestStatus2" && compliance2.Id != compliances[0].Id { + if compliances[0].Status != model.COMPLIANCE_STATUS_RUNNING && compliance2.Id != compliances[0].Id { t.Fatal() } - compliance2.Status = "TestUpdateStatus2" + compliance2.Status = model.COMPLIANCE_STATUS_FAILED Must(store.Compliance().Update(compliance2)) c = store.Compliance().GetAll() result = <-c compliances = result.Data.(model.Compliances) - if compliances[0].Status != "TestUpdateStatus2" && compliance2.Id != compliances[0].Id { + if compliances[0].Status != model.COMPLIANCE_STATUS_FAILED && compliance2.Id != compliances[0].Id { t.Fatal() } -- cgit v1.2.3-1-g7c22 From 5982bd490b074752a1d979f028783e88f1554be7 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Wed, 16 Mar 2016 18:35:53 -0700 Subject: Fixing merge --- .../admin_console/compliance_reports.jsx | 384 -------------------- .../admin_console/compliance_settings.jsx | 271 --------------- .../admin_console/compliance_reports.jsx | 386 +++++++++++++++++++++ .../admin_console/compliance_settings.jsx | 274 +++++++++++++++ 4 files changed, 660 insertions(+), 655 deletions(-) delete mode 100644 web/react/components/admin_console/compliance_reports.jsx delete mode 100644 web/react/components/admin_console/compliance_settings.jsx create mode 100644 webapp/components/admin_console/compliance_reports.jsx create mode 100644 webapp/components/admin_console/compliance_settings.jsx diff --git a/web/react/components/admin_console/compliance_reports.jsx b/web/react/components/admin_console/compliance_reports.jsx deleted file mode 100644 index 2a94b6f1d..000000000 --- a/web/react/components/admin_console/compliance_reports.jsx +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import LoadingScreen from '../loading_screen.jsx'; -import * as Utils from '../../utils/utils.jsx'; -import AdminStore from '../../stores/admin_store.jsx'; -import UserStore from '../../stores/user_store.jsx'; - -import * as Client from '../../utils/client.jsx'; -import * as AsyncClient from '../../utils/async_client.jsx'; - -import {FormattedMessage, FormattedDate, FormattedTime} from 'mm-intl'; - -export default class ComplianceReports extends React.Component { - constructor(props) { - super(props); - - this.onComplianceReportsListenerChange = this.onComplianceReportsListenerChange.bind(this); - this.reload = this.reload.bind(this); - this.runReport = this.runReport.bind(this); - this.getDateTime = this.getDateTime.bind(this); - - this.state = { - reports: AdminStore.getComplianceReports(), - serverError: null - }; - } - - componentDidMount() { - AdminStore.addComplianceReportsChangeListener(this.onComplianceReportsListenerChange); - - if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.EnableCompliance !== 'true') { - return; - } - - AsyncClient.getComplianceReports(); - } - - componentWillUnmount() { - AdminStore.removeComplianceReportsChangeListener(this.onComplianceReportsListenerChange); - } - - onComplianceReportsListenerChange() { - this.setState({ - reports: AdminStore.getComplianceReports() - }); - } - - reload() { - AdminStore.saveComplianceReports(null); - this.setState({ - reports: null, - serverError: null - }); - - AsyncClient.getComplianceReports(); - } - - runReport(e) { - e.preventDefault(); - $('#run-button').button('loading'); - - var job = {}; - job.desc = ReactDOM.findDOMNode(this.refs.desc).value; - job.emails = ReactDOM.findDOMNode(this.refs.emails).value; - job.keywords = ReactDOM.findDOMNode(this.refs.keywords).value; - job.start_at = Date.parse(ReactDOM.findDOMNode(this.refs.from).value); - job.end_at = Date.parse(ReactDOM.findDOMNode(this.refs.to).value); - - Client.saveComplianceReports( - job, - () => { - ReactDOM.findDOMNode(this.refs.emails).value = ''; - ReactDOM.findDOMNode(this.refs.keywords).value = ''; - ReactDOM.findDOMNode(this.refs.desc).value = ''; - ReactDOM.findDOMNode(this.refs.from).value = ''; - ReactDOM.findDOMNode(this.refs.to).value = ''; - this.reload(); - $('#run-button').button('reset'); - }, - (err) => { - this.setState({serverError: err.message}); - $('#run-button').button('reset'); - } - ); - } - - getDateTime(millis) { - const date = new Date(millis); - return ( - - - {' - '} - - - ); - } - - render() { - var content = null; - - if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.EnableCompliance !== 'true') { - return
    ; - } - - if (this.state.reports === null) { - content = ; - } else { - var list = []; - - for (var i = 0; i < this.state.reports.length; i++) { - const report = this.state.reports[i]; - - var params = ''; - if (report.type === 'adhoc') { - params = ( - - {' '}{this.getDateTime(report.start_at)} -
    - {' '}{this.getDateTime(report.end_at)} -
    - {' '}{report.emails} -
    - {' '}{report.keywords} -
    ); - } - - var download = ''; - if (report.status === 'finished') { - download = ( - - - - ); - } - - var status = report.status; - if (report.status === 'finished') { - status = ( - {report.status} - ); - } - - if (report.status === 'failed') { - status = ( - {report.status} - ); - } - - var user = report.user_id; - var profile = UserStore.getProfile(report.user_id); - if (profile) { - user = profile.email; - } - - list[i] = ( - - {download} - {this.getDateTime(report.create_at)} - {status} - {report.count} - {report.type} - {report.desc} - {user} - {params} - - ); - } - - content = ( -
    - - - - - - - - - - - - - - - {list} - -
    - - - - - - - - - - - - - -
    -
    - ); - } - - let serverError = ''; - if (this.state.serverError) { - serverError = ( -
    - -
    - ); - } - - return ( -
    -

    - -

    - - - - - - - - - - - - - - -
    - - -
    - - - - - - - - - - - - - -
    - {serverError} -
    - -
    -
    - {content} -
    -
    - ); - } -} diff --git a/web/react/components/admin_console/compliance_settings.jsx b/web/react/components/admin_console/compliance_settings.jsx deleted file mode 100644 index 8e6ca6340..000000000 --- a/web/react/components/admin_console/compliance_settings.jsx +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Client from '../../utils/client.jsx'; -import * as AsyncClient from '../../utils/async_client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -var holders = defineMessages({ - saving: { - id: 'admin.compliance.saving', - defaultMessage: 'Saving Config...' - }, - directoryExample: { - id: 'admin.compliance.directoryExample', - defaultMessage: 'Ex "./data/"' - } -}); - -class ComplianceSettings extends React.Component { - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - this.handleChange = this.handleChange.bind(this); - this.handleEnable = this.handleEnable.bind(this); - this.handleDisable = this.handleDisable.bind(this); - - this.state = { - saveNeeded: false, - serverError: null, - enable: this.props.config.ComplianceSettings.Enable - }; - } - handleChange() { - this.setState({saveNeeded: true}); - } - handleEnable() { - this.setState({saveNeeded: true, enable: true}); - } - handleDisable() { - this.setState({saveNeeded: true, enable: false}); - } - handleSubmit(e) { - e.preventDefault(); - $('#save-button').button('loading'); - - const config = this.props.config; - config.ComplianceSettings.Enable = this.refs.Enable.checked; - config.ComplianceSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value; - config.ComplianceSettings.EnableDaily = this.refs.EnableDaily.checked; - - Client.saveConfig( - config, - () => { - AsyncClient.getConfig(); - this.setState({ - serverError: null, - saveNeeded: false - }); - $('#save-button').button('reset'); - }, - (err) => { - this.setState({ - serverError: err.message, - saveNeeded: true - }); - $('#save-button').button('reset'); - } - ); - } - render() { - const {formatMessage} = this.props.intl; - let serverError = ''; - if (this.state.serverError) { - serverError =
    ; - } - - let saveClass = 'btn'; - if (this.state.saveNeeded) { - saveClass = 'btn btn-primary'; - } - - const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.Compliance === 'true'; - - let bannerContent; - if (!licenseEnabled) { - bannerContent = ( -
    -
    - -
    -
    - ); - } - - return ( -
    - {bannerContent} -

    - -

    -
    - -
    - -
    - - -

    - -

    -
    -
    - -
    - -
    - -

    - -

    -
    -
    - -
    - -
    - - -

    - -

    -
    -
    - -
    -
    - {serverError} - -
    -
    -
    -
    - ); - } -} -ComplianceSettings.defaultProps = { -}; - -ComplianceSettings.propTypes = { - intl: intlShape.isRequired, - config: React.PropTypes.object -}; - -export default injectIntl(ComplianceSettings); diff --git a/webapp/components/admin_console/compliance_reports.jsx b/webapp/components/admin_console/compliance_reports.jsx new file mode 100644 index 000000000..3dd073ccd --- /dev/null +++ b/webapp/components/admin_console/compliance_reports.jsx @@ -0,0 +1,386 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from '../loading_screen.jsx'; +import * as Utils from '../../utils/utils.jsx'; +import AdminStore from '../../stores/admin_store.jsx'; +import UserStore from '../../stores/user_store.jsx'; + +import * as Client from '../../utils/client.jsx'; +import * as AsyncClient from '../../utils/async_client.jsx'; + +import {FormattedMessage, FormattedDate, FormattedTime} from 'react-intl'; + +import React from 'react'; + +export default class ComplianceReports extends React.Component { + constructor(props) { + super(props); + + this.onComplianceReportsListenerChange = this.onComplianceReportsListenerChange.bind(this); + this.reload = this.reload.bind(this); + this.runReport = this.runReport.bind(this); + this.getDateTime = this.getDateTime.bind(this); + + this.state = { + reports: AdminStore.getComplianceReports(), + serverError: null + }; + } + + componentDidMount() { + AdminStore.addComplianceReportsChangeListener(this.onComplianceReportsListenerChange); + + if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.EnableCompliance !== 'true') { + return; + } + + AsyncClient.getComplianceReports(); + } + + componentWillUnmount() { + AdminStore.removeComplianceReportsChangeListener(this.onComplianceReportsListenerChange); + } + + onComplianceReportsListenerChange() { + this.setState({ + reports: AdminStore.getComplianceReports() + }); + } + + reload() { + AdminStore.saveComplianceReports(null); + this.setState({ + reports: null, + serverError: null + }); + + AsyncClient.getComplianceReports(); + } + + runReport(e) { + e.preventDefault(); + $('#run-button').button('loading'); + + var job = {}; + job.desc = ReactDOM.findDOMNode(this.refs.desc).value; + job.emails = ReactDOM.findDOMNode(this.refs.emails).value; + job.keywords = ReactDOM.findDOMNode(this.refs.keywords).value; + job.start_at = Date.parse(ReactDOM.findDOMNode(this.refs.from).value); + job.end_at = Date.parse(ReactDOM.findDOMNode(this.refs.to).value); + + Client.saveComplianceReports( + job, + () => { + ReactDOM.findDOMNode(this.refs.emails).value = ''; + ReactDOM.findDOMNode(this.refs.keywords).value = ''; + ReactDOM.findDOMNode(this.refs.desc).value = ''; + ReactDOM.findDOMNode(this.refs.from).value = ''; + ReactDOM.findDOMNode(this.refs.to).value = ''; + this.reload(); + $('#run-button').button('reset'); + }, + (err) => { + this.setState({serverError: err.message}); + $('#run-button').button('reset'); + } + ); + } + + getDateTime(millis) { + const date = new Date(millis); + return ( + + + {' - '} + + + ); + } + + render() { + var content = null; + + if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.EnableCompliance !== 'true') { + return
    ; + } + + if (this.state.reports === null) { + content = ; + } else { + var list = []; + + for (var i = 0; i < this.state.reports.length; i++) { + const report = this.state.reports[i]; + + var params = ''; + if (report.type === 'adhoc') { + params = ( + + {' '}{this.getDateTime(report.start_at)} +
    + {' '}{this.getDateTime(report.end_at)} +
    + {' '}{report.emails} +
    + {' '}{report.keywords} +
    ); + } + + var download = ''; + if (report.status === 'finished') { + download = ( + + + + ); + } + + var status = report.status; + if (report.status === 'finished') { + status = ( + {report.status} + ); + } + + if (report.status === 'failed') { + status = ( + {report.status} + ); + } + + var user = report.user_id; + var profile = UserStore.getProfile(report.user_id); + if (profile) { + user = profile.email; + } + + list[i] = ( + + {download} + {this.getDateTime(report.create_at)} + {status} + {report.count} + {report.type} + {report.desc} + {user} + {params} + + ); + } + + content = ( +
    + + + + + + + + + + + + + + + {list} + +
    + + + + + + + + + + + + + +
    +
    + ); + } + + let serverError = ''; + if (this.state.serverError) { + serverError = ( +
    + +
    + ); + } + + return ( +
    +

    + +

    + + + + + + + + + + + + + + +
    + + +
    + + + + + + + + + + + + + +
    + {serverError} +
    + +
    +
    + {content} +
    +
    + ); + } +} diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx new file mode 100644 index 000000000..229984475 --- /dev/null +++ b/webapp/components/admin_console/compliance_settings.jsx @@ -0,0 +1,274 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from '../../utils/client.jsx'; +import * as AsyncClient from '../../utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +import React from 'react'; +import ReactDOM from 'react-dom'; + +var holders = defineMessages({ + saving: { + id: 'admin.compliance.saving', + defaultMessage: 'Saving Config...' + }, + directoryExample: { + id: 'admin.compliance.directoryExample', + defaultMessage: 'Ex "./data/"' + } +}); + +class ComplianceSettings extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleEnable = this.handleEnable.bind(this); + this.handleDisable = this.handleDisable.bind(this); + + this.state = { + saveNeeded: false, + serverError: null, + enable: this.props.config.ComplianceSettings.Enable + }; + } + handleChange() { + this.setState({saveNeeded: true}); + } + handleEnable() { + this.setState({saveNeeded: true, enable: true}); + } + handleDisable() { + this.setState({saveNeeded: true, enable: false}); + } + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + const config = this.props.config; + config.ComplianceSettings.Enable = this.refs.Enable.checked; + config.ComplianceSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value; + config.ComplianceSettings.EnableDaily = this.refs.EnableDaily.checked; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + render() { + const {formatMessage} = this.props.intl; + let serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + let saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.Compliance === 'true'; + + let bannerContent; + if (!licenseEnabled) { + bannerContent = ( +
    +
    + +
    +
    + ); + } + + return ( +
    + {bannerContent} +

    + +

    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    +
    +
    + ); + } +} +ComplianceSettings.defaultProps = { +}; + +ComplianceSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(ComplianceSettings); -- cgit v1.2.3-1-g7c22 From a85f74eac8b6c728f36b91f2ca2108458154d31a Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Wed, 16 Mar 2016 18:36:37 -0700 Subject: skipping version.go file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 04e308504..4c343021e 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,5 @@ api/data/* .ctags tags +model/version.go model/version.go.bak -- cgit v1.2.3-1-g7c22 From 074f15f761ff50388b58e35e8354c139c06a807e Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Wed, 16 Mar 2016 18:52:46 -0700 Subject: Fixing webpack --- webapp/components/admin_console/compliance_reports.jsx | 2 ++ webapp/components/admin_console/compliance_settings.jsx | 1 + 2 files changed, 3 insertions(+) diff --git a/webapp/components/admin_console/compliance_reports.jsx b/webapp/components/admin_console/compliance_reports.jsx index 3dd073ccd..84def2bce 100644 --- a/webapp/components/admin_console/compliance_reports.jsx +++ b/webapp/components/admin_console/compliance_reports.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import LoadingScreen from '../loading_screen.jsx'; import * as Utils from '../../utils/utils.jsx'; import AdminStore from '../../stores/admin_store.jsx'; @@ -12,6 +13,7 @@ import * as AsyncClient from '../../utils/async_client.jsx'; import {FormattedMessage, FormattedDate, FormattedTime} from 'react-intl'; import React from 'react'; +import ReactDOM from 'react-dom'; export default class ComplianceReports extends React.Component { constructor(props) { diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx index 229984475..f77697b5e 100644 --- a/webapp/components/admin_console/compliance_settings.jsx +++ b/webapp/components/admin_console/compliance_settings.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import $ from 'jquery'; import * as Client from '../../utils/client.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; -- cgit v1.2.3-1-g7c22 From 3ca32ee1e562807af1ff323182c455afe75b91f8 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Thu, 17 Mar 2016 11:21:08 -0700 Subject: Fixing localization stuff --- .../admin_console/compliance_settings.jsx | 28 +++++++--------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx index f77697b5e..b7f1a980a 100644 --- a/webapp/components/admin_console/compliance_settings.jsx +++ b/webapp/components/admin_console/compliance_settings.jsx @@ -4,24 +4,14 @@ import $ from 'jquery'; import * as Client from '../../utils/client.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; +import * as Utils from '../../utils/utils.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; import React from 'react'; import ReactDOM from 'react-dom'; -var holders = defineMessages({ - saving: { - id: 'admin.compliance.saving', - defaultMessage: 'Saving Config...' - }, - directoryExample: { - id: 'admin.compliance.directoryExample', - defaultMessage: 'Ex "./data/"' - } -}); - -class ComplianceSettings extends React.Component { +export default class ComplianceSettings extends React.Component { constructor(props) { super(props); @@ -74,7 +64,6 @@ class ComplianceSettings extends React.Component { ); } render() { - const {formatMessage} = this.props.intl; let serverError = ''; if (this.state.serverError) { serverError =
    ; @@ -101,6 +90,9 @@ class ComplianceSettings extends React.Component { ); } + //var exampleDir = Utils.localizeMessage('admin.compliance.directoryExample', 'Ex "./data/"'); + //var saveButtonText = Utils.localizeMessage('admin.compliance.saving', 'Saving Config...'); + return (
    {bannerContent} @@ -179,7 +171,7 @@ class ComplianceSettings extends React.Component { className='form-control' id='Directory' ref='Directory' - placeholder={formatMessage(holders.directoryExample)} + placeholder={Utils.localizeMessage('admin.compliance.directoryExample', 'Ex "./data/"')} defaultValue={this.props.config.ComplianceSettings.Directory} onChange={this.handleChange} disabled={!this.state.enable} @@ -250,7 +242,7 @@ class ComplianceSettings extends React.Component { className={saveClass} onClick={this.handleSubmit} id='save-button' - data-loading-text={' ' + formatMessage(holders.saving)} + data-loading-text={' ' + Utils.localizeMessage('admin.compliance.saving', 'Saving Config...')} > Date: Thu, 17 Mar 2016 11:23:27 -0700 Subject: Fixing localization stuff --- webapp/components/admin_console/compliance_settings.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx index b7f1a980a..fb2ae26f9 100644 --- a/webapp/components/admin_console/compliance_settings.jsx +++ b/webapp/components/admin_console/compliance_settings.jsx @@ -90,9 +90,6 @@ export default class ComplianceSettings extends React.Component { ); } - //var exampleDir = Utils.localizeMessage('admin.compliance.directoryExample', 'Ex "./data/"'); - //var saveButtonText = Utils.localizeMessage('admin.compliance.saving', 'Saving Config...'); - return (
    {bannerContent} -- cgit v1.2.3-1-g7c22