PLT-2115 Adding compliance

This commit is contained in:
=Corey Hulen
2016-03-14 16:07:58 -07:00
parent ea3f25924e
commit 36b17bf99d
36 changed files with 1791 additions and 211 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

132
model/compliance.go Normal file
View File

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

View File

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

19
model/compliance_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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='#'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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