mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
PLT-2115 Adding compliance
This commit is contained in:
112
api/admin.go
112
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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
37
i18n/en.json
37
i18n/en.json
@@ -1811,7 +1811,10 @@
|
||||
"id": "ent.compliance.run_finished.info",
|
||||
"translation": "Compliance export finished for job '{{.JobName}}' exported {{.Count}} records to '{{.FilePath}}'"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "ent.compliance.licence_disable.app_error",
|
||||
"translation": "Compliance functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license."
|
||||
},
|
||||
{
|
||||
"id": "mattermost.security_checks.debug",
|
||||
"translation": "Checking for security update from Mattermost"
|
||||
@@ -2232,6 +2235,30 @@
|
||||
"id": "model.post.is_valid.user_id.app_error",
|
||||
"translation": "Invalid user id"
|
||||
},
|
||||
{
|
||||
"id": "model.compliance.is_valid.id.app_error",
|
||||
"translation": "Invalid Id"
|
||||
},
|
||||
{
|
||||
"id": "model.compliance.is_valid.create_at.app_error",
|
||||
"translation": "Create at must be a valid time"
|
||||
},
|
||||
{
|
||||
"id": "model.compliance.is_valid.desc.app_error",
|
||||
"translation": "Invalid description"
|
||||
},
|
||||
{
|
||||
"id": "model.compliance.is_valid.start_at.app_error",
|
||||
"translation": "From must be a valid time"
|
||||
},
|
||||
{
|
||||
"id": "model.compliance.is_valid.end_at.app_error",
|
||||
"translation": "To must be a valid time"
|
||||
},
|
||||
{
|
||||
"id": "model.compliance.is_valid.start_end_at.app_error",
|
||||
"translation": "To must be greater than From"
|
||||
},
|
||||
{
|
||||
"id": "model.preference.is_valid.category.app_error",
|
||||
"translation": "Invalid category"
|
||||
@@ -2508,6 +2535,14 @@
|
||||
"id": "store.sql_audit.save.saving.app_error",
|
||||
"translation": "We encountered an error saving the audit"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_compliance.save.saving.app_error",
|
||||
"translation": "We encountered an error saving the compliance report"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_compliance.get.finding.app_error",
|
||||
"translation": "We encountered an error retrieving the compliance reports"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_channel.analytics_type_count.app_error",
|
||||
"translation": "We couldn't get channel type counts"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
132
model/compliance.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
19
model/compliance_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
234
store/sql_compliance_store.go
Normal file
234
store/sql_compliance_store.go
Normal 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
|
||||
}
|
||||
210
store/sql_compliance_store_test.go
Normal file
210
store/sql_compliance_store_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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='#'
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
384
web/react/components/admin_console/compliance_reports.jsx
Normal file
384
web/react/components/admin_console/compliance_reports.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
271
web/react/components/admin_console/compliance_settings.jsx
Normal file
271
web/react/components/admin_console/compliance_settings.jsx
Normal 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);
|
||||
@@ -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':
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user