summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2016-03-14 16:07:58 -0700
committer=Corey Hulen <corey@hulen.com>2016-03-15 18:27:45 -0700
commit36b17bf99ddd35c0c223722f8b6f4f1c71b2235e (patch)
tree6bc07d42d19d1d7a5bef26864bb93a1bc91da5f1
parentea3f25924ea64a2dd1e73624c0d30824e1efb240 (diff)
downloadchat-36b17bf99ddd35c0c223722f8b6f4f1c71b2235e.tar.gz
chat-36b17bf99ddd35c0c223722f8b6f4f1c71b2235e.tar.bz2
chat-36b17bf99ddd35c0c223722f8b6f4f1c71b2235e.zip
PLT-2115 Adding compliance
-rw-r--r--api/admin.go112
-rw-r--r--api/admin_test.go3
-rw-r--r--api/license.go20
-rw-r--r--config/config.json34
-rw-r--r--einterfaces/compliance.go2
-rw-r--r--i18n/en.json37
-rw-r--r--mattermost.go22
-rw-r--r--model/client.go36
-rw-r--r--model/compliance.go132
-rw-r--r--model/compliance_post.go18
-rw-r--r--model/compliance_test.go19
-rw-r--r--model/config.go46
-rw-r--r--model/license.go12
-rw-r--r--store/sql_audit_store.go15
-rw-r--r--store/sql_compliance_store.go234
-rw-r--r--store/sql_compliance_store_test.go210
-rw-r--r--store/sql_post_store.go56
-rw-r--r--store/sql_post_store_test.go76
-rw-r--r--store/sql_store.go8
-rw-r--r--store/store.go10
-rw-r--r--utils/config.go2
-rw-r--r--utils/html.go11
-rw-r--r--utils/i18n.go6
-rw-r--r--utils/license.go1
-rw-r--r--web/react/components/admin_console/admin_controller.jsx3
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx17
-rw-r--r--web/react/components/admin_console/audits.jsx55
-rw-r--r--web/react/components/admin_console/compliance_reports.jsx384
-rw-r--r--web/react/components/admin_console/compliance_settings.jsx271
-rw-r--r--web/react/components/audit_table.jsx9
-rw-r--r--web/react/stores/admin_store.jsx30
-rw-r--r--web/react/utils/async_client.jsx26
-rw-r--r--web/react/utils/client.jsx29
-rw-r--r--web/react/utils/constants.jsx1
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss20
-rw-r--r--web/static/i18n/en.json35
36 files changed, 1791 insertions, 211 deletions
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"
@@ -2233,6 +2236,30 @@
"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"
},
@@ -2509,6 +2536,14 @@
"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 = <LegalAndSupportSettingsTab config={this.state.config}/>;
} else if (this.state.selected === 'ldap_settings') {
tab = <LdapSettingsTab config={this.state.config}/>;
+ } else if (this.state.selected === 'compliance_settings') {
+ tab = <ComplianceSettingsTab config={this.state.config}/>;
} else if (this.state.selected === 'license') {
tab = <LicenseSettingsTab config={this.state.config}/>;
} 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 {
</a>
</li>
);
+
+ complianceSettings = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('compliance_settings')}
+ onClick={this.handleClick.bind(this, 'compliance_settings', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.compliance'
+ defaultMessage='Compliance Settings'
+ />
+ </a>
+ </li>
+ );
}
licenseSettings = (
@@ -386,6 +402,7 @@ export default class AdminSidebar extends React.Component {
</a>
</li>
{ldapSettings}
+ {complianceSettings}
<li>
<a
href='#'
diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx
index c519e0a23..907a2561a 100644
--- a/web/react/components/admin_console/audits.jsx
+++ b/web/react/components/admin_console/audits.jsx
@@ -3,6 +3,7 @@
import LoadingScreen from '../loading_screen.jsx';
import AuditTable from '../audit_table.jsx';
+import ComplianceReports from './compliance_reports.jsx';
import AdminStore from '../../stores/admin_store.jsx';
@@ -58,36 +59,40 @@ export default class Audits extends React.Component {
} else {
content = (
<div style={{margin: '10px'}}>
- <AuditTable
- audits={this.state.audits}
- showUserId={true}
- showIp={true}
- showSession={true}
- />
+ <AuditTable
+ audits={this.state.audits}
+ showUserId={true}
+ showIp={true}
+ showSession={true}
+ />
</div>
);
}
return (
- <div className='panel'>
- <h3>
- <FormattedMessage
- id='admin.audits.title'
- defaultMessage='User Activity'
- />
- </h3>
- <button
- type='submit'
- className='btn btn-primary'
- onClick={this.reload}
- >
- <FormattedMessage
- id='admin.audits.reload'
- defaultMessage='Reload'
- />
- </button>
- <div className='log__panel'>
- {content}
+ <div>
+ <ComplianceReports/>
+
+ <div className='panel'>
+ <h3>
+ <FormattedMessage
+ id='admin.audits.title'
+ defaultMessage='User Activity'
+ />
+ </h3>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.reload}
+ >
+ <FormattedMessage
+ id='admin.audits.reload'
+ defaultMessage='Reload'
+ />
+ </button>
+ <div className='audit__panel'>
+ {content}
+ </div>
</div>
</div>
);
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 (
+ <span style={{whiteSpace: 'nowrap'}}>
+ <FormattedDate
+ value={date}
+ day='2-digit'
+ month='short'
+ year='numeric'
+ />
+ {' - '}
+ <FormattedTime
+ value={date}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ </span>
+ );
+ }
+
+ render() {
+ var content = null;
+
+ if (global.window.mm_license.IsLicensed !== 'true' || global.window.mm_config.EnableCompliance !== 'true') {
+ return <div/>;
+ }
+
+ if (this.state.reports === null) {
+ content = <LoadingScreen/>;
+ } 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 = (
+ <span>
+ <FormattedMessage
+ id='admin.compliance_reports.from'
+ defaultMessage='From:'
+ />{' '}{this.getDateTime(report.start_at)}
+ <br/>
+ <FormattedMessage
+ id='admin.compliance_reports.to'
+ defaultMessage='To:'
+ />{' '}{this.getDateTime(report.end_at)}
+ <br/>
+ <FormattedMessage
+ id='admin.compliance_reports.emails'
+ defaultMessage='Emails:'
+ />{' '}{report.emails}
+ <br/>
+ <FormattedMessage
+ id='admin.compliance_reports.keywords'
+ defaultMessage='Keywords:'
+ />{' '}{report.keywords}
+ </span>);
+ }
+
+ var download = '';
+ if (report.status === 'finished') {
+ download = (
+ <a href={'/api/v1/admin/download_compliance_report/' + report.id}>
+ <FormattedMessage
+ id='admin.compliance_table.download'
+ defaultMessage='Download'
+ />
+ </a>
+ );
+ }
+
+ var status = report.status;
+ if (report.status === 'finished') {
+ status = (
+ <span style={{color: 'green'}}>{report.status}</span>
+ );
+ }
+
+ if (report.status === 'failed') {
+ status = (
+ <span style={{color: 'red'}}>{report.status}</span>
+ );
+ }
+
+ var user = report.user_id;
+ var profile = UserStore.getProfile(report.user_id);
+ if (profile) {
+ user = profile.email;
+ }
+
+ list[i] = (
+ <tr key={report.id}>
+ <td style={{whiteSpace: 'nowrap'}}>{download}</td>
+ <td>{this.getDateTime(report.create_at)}</td>
+ <td>{status}</td>
+ <td>{report.count}</td>
+ <td>{report.type}</td>
+ <td style={{whiteSpace: 'nowrap'}}>{report.desc}</td>
+ <td>{user}</td>
+ <td style={{whiteSpace: 'nowrap'}}>{params}</td>
+ </tr>
+ );
+ }
+
+ content = (
+ <div style={{margin: '10px'}}>
+ <table className='table'>
+ <thead>
+ <tr>
+ <th></th>
+ <th>
+ <FormattedMessage
+ id='admin.compliance_table.timestamp'
+ defaultMessage='Timestamp'
+ />
+ </th>
+ <th>
+ <FormattedMessage
+ id='admin.compliance_table.status'
+ defaultMessage='Status'
+ />
+ </th>
+ <th>
+ <FormattedMessage
+ id='admin.compliance_table.records'
+ defaultMessage='Records'
+ />
+ </th>
+ <th>
+ <FormattedMessage
+ id='admin.compliance_table.type'
+ defaultMessage='Type'
+ />
+ </th>
+ <th>
+ <FormattedMessage
+ id='admin.compliance_table.desc'
+ defaultMessage='Description'
+ />
+ </th>
+ <th>
+ <FormattedMessage
+ id='admin.compliance_table.userId'
+ defaultMessage='Requested By'
+ />
+ </th>
+ <th>
+ <FormattedMessage
+ id='admin.compliance_table.params'
+ defaultMessage='Params'
+ />
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ {list}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+
+ let serverError = '';
+ if (this.state.serverError) {
+ serverError = (
+ <div
+ className='form-group has-error'
+ style={{marginTop: '10px'}}
+ >
+ <label className='control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ return (
+ <div className='panel'>
+ <h3>
+ <FormattedMessage
+ id='admin.compliance_reports.title'
+ defaultMessage='Compliance Reports'
+ />
+ </h3>
+
+ <table>
+ <tbody>
+ <tr>
+ <td colSpan='5'
+ style={{paddingBottom: '6px'}}
+ >
+ <FormattedMessage
+ id='admin.compliance_reports.desc'
+ defaultMessage='Job Name:'
+ />
+ <input
+ style={{width: '425px'}}
+ type='text'
+ className='form-control'
+ id='desc'
+ ref='desc'
+ placeholder={Utils.localizeMessage('admin.compliance_reports.desc_placeholder', 'Ex "Audit 445 for HR"')}
+ />
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <FormattedMessage
+ id='admin.compliance_reports.from'
+ defaultMessage='From:'
+ />
+ <input
+ type='text'
+ className='form-control'
+ id='from'
+ ref='from'
+ placeholder={Utils.localizeMessage('admin.compliance_reports.from_placeholder', 'Ex "2016-03-11"')}
+ />
+ </td>
+ <td style={{paddingLeft: '4px'}}>
+ <FormattedMessage
+ id='admin.compliance_reports.to'
+ defaultMessage='To:'
+ />
+ <input
+ type='text'
+ className='form-control'
+ id='to'
+ ref='to'
+ placeholder={Utils.localizeMessage('admin.compliance_reports.to_placeholder', 'Ex "2016-03-15"')}
+ />
+ </td>
+ <td style={{paddingLeft: '4px'}}>
+ <FormattedMessage
+ id='admin.compliance_reports.emails'
+ defaultMessage='Emails:'
+ />
+ <input
+ style={{width: '325px'}}
+ type='text'
+ className='form-control'
+ id='emails'
+ ref='emails'
+ placeholder={Utils.localizeMessage('admin.compliance_reports.emails_placeholder', 'Ex "bill@example.com, bob@example.com"')}
+ />
+ </td>
+ <td style={{paddingLeft: '4px'}}>
+ <FormattedMessage
+ id='admin.compliance_reports.keywords'
+ defaultMessage='Keywords:'
+ />
+ <input
+ style={{width: '250px'}}
+ type='text'
+ className='form-control'
+ id='keywords'
+ ref='keywords'
+ placeholder={Utils.localizeMessage('admin.compliance_reports.keywords_placeholder', 'Ex "shorting stock"')}
+ />
+ </td>
+ <td>
+ <button
+ id='run-button'
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.runReport}
+ style={{marginTop: '20px', marginLeft: '20px'}}
+ >
+ <FormattedMessage
+ id='admin.compliance_reports.run'
+ defaultMessage='Run'
+ />
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ {serverError}
+ <div style={{marginTop: '20px'}}>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.reload}
+ >
+ <FormattedMessage
+ id='admin.compliance_reports.reload'
+ defaultMessage='Reload'
+ />
+ </button>
+ </div>
+ <div className='compliance__panel'>
+ {content}
+ </div>
+ </div>
+ );
+ }
+}
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 = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ 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 = (
+ <div className='banner warning'>
+ <div className='banner__content'>
+ <FormattedHTMLMessage
+ id='admin.compliance.noLicense'
+ defaultMessage='<h4 class="banner__heading">Note:</h4><p>Compliance is an enterprise feature. Your current license does not support Compliance. Click <a href="http://mattermost.com"target="_blank">here</a> for information and pricing on enterprise licenses.</p>'
+ />
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ {bannerContent}
+ <h3>
+ <FormattedMessage
+ id='admin.compliance.title'
+ defaultMessage='Compliance Settings'
+ />
+ </h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Enable'
+ >
+ <FormattedMessage
+ id='admin.compliance.enableTitle'
+ defaultMessage='Enable Compliance:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Enable'
+ value='true'
+ ref='Enable'
+ defaultChecked={this.props.config.ComplianceSettings.Enable}
+ onChange={this.handleEnable}
+ disabled={!licenseEnabled}
+ />
+ <FormattedMessage
+ id='admin.compliance.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Enable'
+ value='false'
+ defaultChecked={!this.props.config.ComplianceSettings.Enable}
+ onChange={this.handleDisable}
+ />
+ <FormattedMessage
+ id='admin.compliance.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.compliance.enableDesc'
+ defaultMessage='When true, Mattermost allows compliance reporting'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Directory'
+ >
+ <FormattedMessage
+ id='admin.compliance.directoryTitle'
+ defaultMessage='Compliance Directory Location:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Directory'
+ ref='Directory'
+ placeholder={formatMessage(holders.directoryExample)}
+ defaultValue={this.props.config.ComplianceSettings.Directory}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.compliance.directoryDescription'
+ defaultMessage='Directory to which compliance reports are written. If blank, will be set to ./data/.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableDaily'
+ >
+ <FormattedMessage
+ id='admin.compliance.enableDailyTitle'
+ defaultMessage='Enable Daily Report:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDaily'
+ value='true'
+ ref='EnableDaily'
+ defaultChecked={this.props.config.ComplianceSettings.EnableDaily}
+ onChange={this.handleChange}
+ disabled={!this.state.enable}
+ />
+ <FormattedMessage
+ id='admin.compliance.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableDaily'
+ value='false'
+ defaultChecked={!this.props.config.ComplianceSettings.EnableDaily}
+ disabled={!this.state.enable}
+ />
+ <FormattedMessage
+ id='admin.compliance.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.compliance.enableDesc'
+ defaultMessage='When true, Mattermost will generate a daily compliance report.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ {serverError}
+ <button
+ disabled={!this.state.saveNeeded}
+ type='submit'
+ className={saveClass}
+ onClick={this.handleSubmit}
+ id='save-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
+ >
+ <FormattedMessage
+ id='admin.compliance.save'
+ defaultMessage='Save'
+ />
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+ }
+}
+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 = <td>{auditInfo.userId}</td>;
+ var profile = UserStore.getProfile(auditInfo.userId);
+ if (profile) {
+ uContent = <td>{profile.email}</td>;
+ } else {
+ uContent = <td>{auditInfo.userId}</td>;
+ }
}
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": "<h4 class=\"banner__heading\">Note:</h4><p>Compliance is an enterprise feature. Your current license does not support Compliance. Click <a href=\"http://mattermost.com\" target=\"_blank\">here</a> for information and pricing on enterprise licenses.</p>",
+ "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, <a href=\"http://mattermost.com\" target=\"_blank\">disable all Enterprise Edition features on this server</a>. 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",