Feature/audit certificate upload (#30223)
* feat: Add certificate upload option for audit logging settings * Commit current changes * Additions * MM-62944 Fix fileupload settings not being clickable * Support for uploading a cert for experimental audit logging cert. Pre cloud implementation in the backend * Forgot to add new hook * Add support for setting custom audit log certifcates in Cloud * Permissions * I18n * Change order * Linter fixes * Linter fixes, add openapi spec * additions for openapi * More openapi fixes because it won't run locally * Undo, cursor went rogue * newline fix * Align types properly * Fix i18n * Fix i18n AGAIN * Fix error * Update api/v4/source/audit_logging.yaml --------- Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -59,6 +59,7 @@ build-v4: node_modules playbooks
|
|||||||
@cat $(V4_SRC)/metrics.yaml >> $(V4_YAML)
|
@cat $(V4_SRC)/metrics.yaml >> $(V4_YAML)
|
||||||
@cat $(V4_SRC)/scheduled_post.yaml >> $(V4_YAML)
|
@cat $(V4_SRC)/scheduled_post.yaml >> $(V4_YAML)
|
||||||
@cat $(V4_SRC)/custom_profile_attributes.yaml >> $(V4_YAML)
|
@cat $(V4_SRC)/custom_profile_attributes.yaml >> $(V4_YAML)
|
||||||
|
@cat $(V4_SRC)/audit_logging.yaml >> $(V4_YAML)
|
||||||
@if [ -r $(PLAYBOOKS_SRC)/paths.yaml ]; then cat $(PLAYBOOKS_SRC)/paths.yaml >> $(V4_YAML); fi
|
@if [ -r $(PLAYBOOKS_SRC)/paths.yaml ]; then cat $(PLAYBOOKS_SRC)/paths.yaml >> $(V4_YAML); fi
|
||||||
@if [ -r $(PLAYBOOKS_SRC)/merged-definitions.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-definitions.yaml >> $(V4_YAML); else cat $(V4_SRC)/definitions.yaml >> $(V4_YAML); fi
|
@if [ -r $(PLAYBOOKS_SRC)/merged-definitions.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-definitions.yaml >> $(V4_YAML); else cat $(V4_SRC)/definitions.yaml >> $(V4_YAML); fi
|
||||||
@echo Extracting code samples
|
@echo Extracting code samples
|
||||||
|
|||||||
68
api/v4/source/audit_logging.yaml
Обычный файл
68
api/v4/source/audit_logging.yaml
Обычный файл
@@ -0,0 +1,68 @@
|
|||||||
|
/api/v4/audit_logs/certificate:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- audit_logs
|
||||||
|
summary: Upload audit log certificate
|
||||||
|
description: |
|
||||||
|
Upload the certificate to be used for TLS verification with the audit log service.
|
||||||
|
|
||||||
|
##### Permissions
|
||||||
|
Must have `sysconsole_write_experimental_features` permission.
|
||||||
|
|
||||||
|
__Minimum server version__: 10.9
|
||||||
|
operationId: AddAuditLogCertificate
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
certificate:
|
||||||
|
description: The certificate file
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
required:
|
||||||
|
- certificate
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Certificate upload successful
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StatusOK"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
"413":
|
||||||
|
$ref: "#/components/responses/TooLarge"
|
||||||
|
"501":
|
||||||
|
$ref: "#/components/responses/NotImplemented"
|
||||||
|
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- audit_logs
|
||||||
|
summary: Remove audit log certificate
|
||||||
|
description: |
|
||||||
|
Delete the current certificate being used with the audit log service.
|
||||||
|
|
||||||
|
##### Permissions
|
||||||
|
Must have `sysconsole_write_experimental_features` permission.
|
||||||
|
|
||||||
|
__Minimum server version__: 9.5
|
||||||
|
operationId: RemoveAuditLogCertificate
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Certificate deletion successful
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/StatusOK"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
"501":
|
||||||
|
$ref: "#/components/responses/NotImplemented"
|
||||||
@@ -80,7 +80,7 @@ tags:
|
|||||||
#### Session Token
|
#### Session Token
|
||||||
|
|
||||||
|
|
||||||
Make an HTTP POST to `your-mattermost-url.com/api/v4/users/login` with a JSON body indicating the user’s `login_id`, `password` and optionally the MFA `token`. The `login_id` can be an email, username or an AD/LDAP ID depending on the system's configuration.
|
Make an HTTP POST to `your-mattermost-url.com/api/v4/users/login` with a JSON body indicating the user's `login_id`, `password` and optionally the MFA `token`. The `login_id` can be an email, username or an AD/LDAP ID depending on the system's configuration.
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -561,6 +561,8 @@ tags:
|
|||||||
description: Endpoints related to export files.
|
description: Endpoints related to export files.
|
||||||
- name: metrics
|
- name: metrics
|
||||||
description: Endpoints related to metrics, including the Client Performance Monitoring feature.
|
description: Endpoints related to metrics, including the Client Performance Monitoring feature.
|
||||||
|
- name: audit_logs
|
||||||
|
description: Endpoints for managing audit log certificates and configuration.
|
||||||
x-tagGroups:
|
x-tagGroups:
|
||||||
- name: Overview
|
- name: Overview
|
||||||
tags:
|
tags:
|
||||||
@@ -618,6 +620,7 @@ x-tagGroups:
|
|||||||
- reports
|
- reports
|
||||||
- custom profile attributes
|
- custom profile attributes
|
||||||
- metrics
|
- metrics
|
||||||
|
- audit_logs
|
||||||
servers:
|
servers:
|
||||||
- url: http://your-mattermost-url.com
|
- url: http://your-mattermost-url.com
|
||||||
- url: https://your-mattermost-url.com
|
- url: https://your-mattermost-url.com
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ type Routes struct {
|
|||||||
CustomProfileAttributesFields *mux.Router // 'api/v4/custom_profile_attributes/fields'
|
CustomProfileAttributesFields *mux.Router // 'api/v4/custom_profile_attributes/fields'
|
||||||
CustomProfileAttributesField *mux.Router // 'api/v4/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
|
CustomProfileAttributesField *mux.Router // 'api/v4/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
|
||||||
CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
|
CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
|
||||||
|
|
||||||
|
AuditLogs *mux.Router // 'api/v4/audit_logs'
|
||||||
}
|
}
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
@@ -298,6 +300,8 @@ func Init(srv *app.Server) (*API, error) {
|
|||||||
api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
|
api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
|
||||||
api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
|
api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
|
||||||
|
|
||||||
|
api.BaseRoutes.AuditLogs = api.BaseRoutes.APIRoot.PathPrefix("/audit_logs").Subrouter()
|
||||||
|
|
||||||
api.InitUser()
|
api.InitUser()
|
||||||
api.InitBot()
|
api.InitBot()
|
||||||
api.InitTeam()
|
api.InitTeam()
|
||||||
@@ -349,6 +353,7 @@ func Init(srv *app.Server) (*API, error) {
|
|||||||
api.InitClientPerformanceMetrics()
|
api.InitClientPerformanceMetrics()
|
||||||
api.InitScheduledPost()
|
api.InitScheduledPost()
|
||||||
api.InitCustomProfileAttributes()
|
api.InitCustomProfileAttributes()
|
||||||
|
api.InitAuditLogging()
|
||||||
|
|
||||||
// If we allow testing then listen for manual testing URL hits
|
// If we allow testing then listen for manual testing URL hits
|
||||||
if *srv.Config().ServiceSettings.EnableTesting {
|
if *srv.Config().ServiceSettings.EnableTesting {
|
||||||
|
|||||||
84
server/channels/api4/audit_logging.go
Обычный файл
84
server/channels/api4/audit_logging.go
Обычный файл
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package api4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
"github.com/mattermost/mattermost/server/v8/channels/audit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (api *API) InitAuditLogging() {
|
||||||
|
api.BaseRoutes.AuditLogs.Handle("/certificate", api.APISessionRequired(addAuditLogCertificate)).Methods(http.MethodPost)
|
||||||
|
api.BaseRoutes.AuditLogs.Handle("/certificate", api.APISessionRequired(removeAuditLogCertificate)).Methods(http.MethodDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAuditLogCertificateRequest(r *http.Request, maxFileSize int64) (*multipart.FileHeader, *model.AppError) {
|
||||||
|
err := r.ParseMultipartForm(maxFileSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("addAuditLogCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := r.MultipartForm
|
||||||
|
|
||||||
|
fileArray, ok := m.File["certificate"]
|
||||||
|
if !ok || len(fileArray) == 0 {
|
||||||
|
return nil, model.NewAppError("addAuditLogCertificate", "api.admin.add_certificate.no_file.app_error", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileArray) > 1 {
|
||||||
|
return nil, model.NewAppError("addAuditLogCertificate", "api.admin.add_certificate.multiple_files.app_error", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileArray[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAuditLogCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.Logger.Debug("addAuditLogCertificate")
|
||||||
|
|
||||||
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteExperimentalFeatures) {
|
||||||
|
c.SetPermissionError(model.PermissionSysconsoleWriteExperimentalFeatures)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData, err := parseAuditLogCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRec := c.MakeAuditRecord("addAuditLogCertificate", audit.Fail)
|
||||||
|
defer c.LogAuditRec(auditRec)
|
||||||
|
audit.AddEventParameter(auditRec, "filename", fileData.Filename)
|
||||||
|
|
||||||
|
if err := c.App.AddAuditLogCertificate(c.AppContext, fileData); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRec.Success()
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAuditLogCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.Logger.Debug("removeAuditLogCertificate")
|
||||||
|
|
||||||
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteExperimentalFeatures) {
|
||||||
|
c.SetPermissionError(model.PermissionSysconsoleWriteExperimentalFeatures)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRec := c.MakeAuditRecord("removeAuditLogCertificate", audit.Fail)
|
||||||
|
defer c.LogAuditRec(auditRec)
|
||||||
|
|
||||||
|
if err := c.App.RemoveAuditLogCertificate(c.AppContext); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRec.Success()
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
@@ -28,6 +30,10 @@ var (
|
|||||||
LevelCLI = mlog.LvlAuditCLI
|
LevelCLI = mlog.LvlAuditCLI
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuditCertificateFilename = "audit_certificate.pem"
|
||||||
|
)
|
||||||
|
|
||||||
func (a *App) GetAudits(rctx request.CTX, userID string, limit int) (model.Audits, *model.AppError) {
|
func (a *App) GetAudits(rctx request.CTX, userID string, limit int) (model.Audits, *model.AppError) {
|
||||||
audits, err := a.Srv().Store().Audit().Get(userID, 0, limit)
|
audits, err := a.Srv().Store().Audit().Get(userID, 0, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -156,3 +162,66 @@ func (s *Server) onAuditTargetQueueFull(qname string, maxQSize int) bool {
|
|||||||
func (s *Server) onAuditError(err error) {
|
func (s *Server) onAuditError(err error) {
|
||||||
s.Log().Error("Audit Error", mlog.Err(err))
|
s.Log().Error("Audit Error", mlog.Err(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) AddAuditLogCertificate(rctx request.CTX, fileData *multipart.FileHeader) *model.AppError {
|
||||||
|
file, err := fileData.Open()
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("AddAuditLogCertificate", "api.admin.add_certificate.open.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("AddAuditLogCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.Srv().platform.SetConfigFile(AuditCertificateFilename, data)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("AddAuditLogCertificate", "api.admin.add_certificate.saving.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := a.Config().Clone()
|
||||||
|
|
||||||
|
*cfg.ExperimentalAuditSettings.Certificate = AuditCertificateFilename
|
||||||
|
|
||||||
|
if err := cfg.IsValid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
|
||||||
|
|
||||||
|
if a.License().IsCloud() {
|
||||||
|
err = a.Cloud().CreateAuditLoggingCert(rctx.Session().UserId, fileData)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("AddAuditLogCertificate", "api.admin.add_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) RemoveAuditLogCertificate(rctx request.CTX) *model.AppError {
|
||||||
|
err := a.Srv().platform.RemoveConfigFile(AuditCertificateFilename)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("RemoveAuditLogCertificate", "api.admin.remove_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := a.Config().Clone()
|
||||||
|
|
||||||
|
*cfg.ExperimentalAuditSettings.Certificate = ""
|
||||||
|
|
||||||
|
if err := cfg.IsValid(); err != nil {
|
||||||
|
return model.NewAppError("RemoveAuditLogCertificate", "api.admin.remove_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
|
||||||
|
|
||||||
|
if a.License().IsCloud() {
|
||||||
|
err = a.Cloud().RemoveAuditLoggingCert(rctx.Session().UserId)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewAppError("RemoveAuditLogCertificate", "api.admin.remove_certificate.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
package einterfaces
|
package einterfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,4 +39,7 @@ type CloudInterface interface {
|
|||||||
ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
|
ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
|
||||||
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
|
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
|
||||||
GetInstallation(userID string) (*model.Installation, error)
|
GetInstallation(userID string) (*model.Installation, error)
|
||||||
|
|
||||||
|
RemoveAuditLoggingCert(userID string) error
|
||||||
|
CreateAuditLoggingCert(userID string, fileData *multipart.FileHeader) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
multipart "mime/multipart"
|
||||||
|
|
||||||
model "github.com/mattermost/mattermost/server/public/model"
|
model "github.com/mattermost/mattermost/server/public/model"
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
@@ -92,6 +94,24 @@ func (_m *CloudInterface) CheckCWSConnection(userId string) error {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAuditLoggingCert provides a mock function with given fields: userID, fileData
|
||||||
|
func (_m *CloudInterface) CreateAuditLoggingCert(userID string, fileData *multipart.FileHeader) error {
|
||||||
|
ret := _m.Called(userID, fileData)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for CreateAuditLoggingCert")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string, *multipart.FileHeader) error); ok {
|
||||||
|
r0 = rf(userID, fileData)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount
|
// CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount
|
||||||
func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) {
|
func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) {
|
||||||
ret := _m.Called(userID, userCount)
|
ret := _m.Called(userID, userCount)
|
||||||
@@ -465,6 +485,24 @@ func (_m *CloudInterface) InvalidateCaches() error {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveAuditLoggingCert provides a mock function with given fields: userID
|
||||||
|
func (_m *CloudInterface) RemoveAuditLoggingCert(userID string) error {
|
||||||
|
ret := _m.Called(userID)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for RemoveAuditLoggingCert")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(userID)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// SubscribeToNewsletter provides a mock function with given fields: userID, req
|
// SubscribeToNewsletter provides a mock function with given fields: userID, req
|
||||||
func (_m *CloudInterface) SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error {
|
func (_m *CloudInterface) SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error {
|
||||||
ret := _m.Called(userID, req)
|
ret := _m.Called(userID, req)
|
||||||
|
|||||||
@@ -59,10 +59,18 @@
|
|||||||
"id": "api.acknowledgement.save.archived_channel.app_error",
|
"id": "api.acknowledgement.save.archived_channel.app_error",
|
||||||
"translation": "You cannot save an acknowledgment in an archived channel."
|
"translation": "You cannot save an acknowledgment in an archived channel."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.add_certificate.app_error",
|
||||||
|
"translation": "Failed to add certificate."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.admin.add_certificate.array.app_error",
|
"id": "api.admin.add_certificate.array.app_error",
|
||||||
"translation": "No file under 'certificate' in request."
|
"translation": "No file under 'certificate' in request."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.add_certificate.multiple_files.app_error",
|
||||||
|
"translation": "Too many files under 'certificate' in request."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.admin.add_certificate.no_file.app_error",
|
"id": "api.admin.add_certificate.no_file.app_error",
|
||||||
"translation": "No file under 'certificate' in request."
|
"translation": "No file under 'certificate' in request."
|
||||||
@@ -95,6 +103,10 @@
|
|||||||
"id": "api.admin.ldap.not_available.app_error",
|
"id": "api.admin.ldap.not_available.app_error",
|
||||||
"translation": "LDAP is not available."
|
"translation": "LDAP is not available."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.remove_certificate.app_error",
|
||||||
|
"translation": "Failed to remove certificate."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.admin.remove_certificate.delete.app_error",
|
"id": "api.admin.remove_certificate.delete.app_error",
|
||||||
"translation": "An error occurred while deleting the certificate."
|
"translation": "An error occurred while deleting the certificate."
|
||||||
|
|||||||
@@ -1538,6 +1538,7 @@ type ExperimentalAuditSettings struct {
|
|||||||
FileCompress *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
FileCompress *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||||
FileMaxQueueSize *int `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
FileMaxQueueSize *int `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||||
AdvancedLoggingJSON json.RawMessage `access:"experimental_features"`
|
AdvancedLoggingJSON json.RawMessage `access:"experimental_features"`
|
||||||
|
Certificate *string `access:"experimental_features"` // telemetry: none
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExperimentalAuditSettings) SetDefaults() {
|
func (s *ExperimentalAuditSettings) SetDefaults() {
|
||||||
@@ -1572,6 +1573,10 @@ func (s *ExperimentalAuditSettings) SetDefaults() {
|
|||||||
if utils.IsEmptyJSON(s.AdvancedLoggingJSON) {
|
if utils.IsEmptyJSON(s.AdvancedLoggingJSON) {
|
||||||
s.AdvancedLoggingJSON = []byte("{}")
|
s.AdvancedLoggingJSON = []byte("{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Certificate == nil {
|
||||||
|
s.Certificate = NewPointer("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAdvancedLoggingConfig returns the advanced logging config as a []byte.
|
// GetAdvancedLoggingConfig returns the advanced logging config as a []byte.
|
||||||
|
|||||||
@@ -234,6 +234,24 @@ export async function uploadIdpSamlCertificate(file, success, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadAuditCertificate(fileData, success, error) {
|
||||||
|
const {data, error: err} = await dispatch(AdminActions.uploadAuditCertificate(fileData));
|
||||||
|
if (data && success) {
|
||||||
|
success('audit.crt');
|
||||||
|
} else if (err && error) {
|
||||||
|
error({id: err.server_error_id, ...err});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeAuditCertificate(success, error) {
|
||||||
|
const {data, error: err} = await dispatch(AdminActions.removeAuditCertificate());
|
||||||
|
if (data && success) {
|
||||||
|
success(data);
|
||||||
|
} else if (err && error) {
|
||||||
|
error({id: err.server_error_id, ...err});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function removePublicSamlCertificate(success, error) {
|
export async function removePublicSamlCertificate(success, error) {
|
||||||
const {data, error: err} = await dispatch(AdminActions.removePublicSamlCertificate());
|
const {data, error: err} = await dispatch(AdminActions.removePublicSamlCertificate());
|
||||||
if (data && success) {
|
if (data && success) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {ID_PATH_PATTERN} from 'utils/path';
|
|||||||
import {getSiteURL} from 'utils/url';
|
import {getSiteURL} from 'utils/url';
|
||||||
|
|
||||||
import * as DefinitionConstants from './admin_definition_constants';
|
import * as DefinitionConstants from './admin_definition_constants';
|
||||||
|
import AuditLoggingCertificateUploadSetting from './audit_logging';
|
||||||
import Audits from './audits';
|
import Audits from './audits';
|
||||||
import {searchableStrings as auditSearchableStrings} from './audits/audits';
|
import {searchableStrings as auditSearchableStrings} from './audits/audits';
|
||||||
import BillingHistory, {searchableStrings as billingHistorySearchableStrings} from './billing/billing_history';
|
import BillingHistory, {searchableStrings as billingHistorySearchableStrings} from './billing/billing_history';
|
||||||
@@ -6715,6 +6716,15 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
return JSON.parse(displayVal);
|
return JSON.parse(displayVal);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'custom',
|
||||||
|
component: AuditLoggingCertificateUploadSetting,
|
||||||
|
label: defineMessage({id: 'admin.audit_logging_experimental.certificate.title', defaultMessage: 'Certificate'}),
|
||||||
|
key: 'ExperimentalAuditSettings.Certificate',
|
||||||
|
help_text: defineMessage({id: 'admin.audit_logging_experimental.certificate.help_text', defaultMessage: 'The certificate file used for audit logging encryption.'}),
|
||||||
|
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||||
|
isHidden: it.not(it.licensedForFeature('Cloud')),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
152
webapp/channels/src/components/admin_console/audit_logging/index.tsx
Обычный файл
152
webapp/channels/src/components/admin_console/audit_logging/index.tsx
Обычный файл
@@ -0,0 +1,152 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {ComponentType} from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
import type {IntlShape} from 'react-intl';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import {removeAuditCertificate, uploadAuditCertificate} from 'actions/admin_actions';
|
||||||
|
|
||||||
|
import useGetCloudInstallationStatus from 'components/common/hooks/useGetCloudInstallationStatus';
|
||||||
|
import WithTooltip from 'components/with_tooltip';
|
||||||
|
|
||||||
|
import FileUploadSetting from '../file_upload_setting';
|
||||||
|
import RemoveFileSetting from '../remove_file_setting';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id?: string;
|
||||||
|
config: any;
|
||||||
|
license: any;
|
||||||
|
intl: IntlShape;
|
||||||
|
value: any;
|
||||||
|
onChange: (id: string, value: string) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
setByEnv: boolean;
|
||||||
|
label: string;
|
||||||
|
helpText: React.JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuditLoggingCertificateUploadSetting: React.FC<Props> = (props: Props) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
setByEnv,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
value,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {status: installationStatus, refetchStatus} = useGetCloudInstallationStatus(true);
|
||||||
|
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
const [fileValue, setFileValue] = React.useState<string | null>(value || null); // State for the file name
|
||||||
|
const [fileError, setFileError] = React.useState<string | null>(null); //State for file error
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
setFileValue(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return (<></>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (id: string, value: string) => {
|
||||||
|
onChange(id, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAction = (successCallback: () => void, errorCallback: (error: any) => void) => {
|
||||||
|
removeAuditCertificate(successCallback, errorCallback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadAction = (file: File, successCallback: (filename: string) => void, errorCallback: (error: any) => void) => {
|
||||||
|
uploadAuditCertificate(file, successCallback, errorCallback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const withTooltip = <P extends object>(Component: ComponentType<P>, tooltipText: string): React.FC<P> => {
|
||||||
|
if (disabled || installationStatus === 'stable') {
|
||||||
|
return (props: P) => <Component {...props}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (props: P) => (
|
||||||
|
<WithTooltip title={tooltipText}>
|
||||||
|
<div>
|
||||||
|
<Component {...props}/>
|
||||||
|
</div>
|
||||||
|
</WithTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tooltipText = formatMessage({id: 'admin.audit_logging_experimental.certificate.tooltip', defaultMessage: 'A previous update is still in progress. Please wait.'});
|
||||||
|
|
||||||
|
const WrappedRemoveFileSetting = withTooltip(RemoveFileSetting, tooltipText);
|
||||||
|
const WrappedFileUploadSetting = withTooltip(FileUploadSetting, tooltipText);
|
||||||
|
|
||||||
|
if (fileValue) {
|
||||||
|
const removeFile = (id: string, callback: () => void) => {
|
||||||
|
const successCallback = () => {
|
||||||
|
handleChange(id, '');
|
||||||
|
setFileValue(null);
|
||||||
|
setFileError(null);
|
||||||
|
refetchStatus();
|
||||||
|
};
|
||||||
|
const errorCallback = (error: any) => {
|
||||||
|
callback();
|
||||||
|
setFileValue(null);
|
||||||
|
setFileError(error.message);
|
||||||
|
refetchStatus();
|
||||||
|
};
|
||||||
|
removeAction(successCallback, errorCallback);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<WrappedRemoveFileSetting
|
||||||
|
id={id}
|
||||||
|
label={label}
|
||||||
|
helpText={formatMessage({id: 'admin.audit_logging_experimental.certificate.remove_help_text', defaultMessage: 'Remove the certificate used for audit logging encryption.'})}
|
||||||
|
removeButtonText={formatMessage({id: 'admin.audit_logging_experimental.certificate.remove_button', defaultMessage: 'Remove Certificate'})}
|
||||||
|
removingText={formatMessage({id: 'admin.audit_logging_experimental.certificate.removing', defaultMessage: 'Removing Certificate...'})}
|
||||||
|
fileName={fileValue}
|
||||||
|
onSubmit={removeFile}
|
||||||
|
disabled={disabled || installationStatus !== 'stable'}
|
||||||
|
setByEnv={setByEnv}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFile = (id: string, file: File, callback: (error?: string) => void) => {
|
||||||
|
const successCallback = (filename: string) => {
|
||||||
|
handleChange(id, filename);
|
||||||
|
setFileValue(filename);
|
||||||
|
setFileError(null);
|
||||||
|
refetchStatus();
|
||||||
|
if (callback && typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const errorCallback = (error: any) => {
|
||||||
|
if (callback && typeof callback === 'function') {
|
||||||
|
callback(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
uploadAction(file, successCallback, errorCallback);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WrappedFileUploadSetting
|
||||||
|
id={id}
|
||||||
|
label={label}
|
||||||
|
helpText={helpText}
|
||||||
|
uploadingText={formatMessage({id: 'admin.audit_logging_experimental.certificate.uploading', defaultMessage: 'Uploading Certificate...'})}
|
||||||
|
disabled={disabled || installationStatus !== 'stable'}
|
||||||
|
fileType={'.crt,.cer,.cert,.pem'}
|
||||||
|
onSubmit={uploadFile}
|
||||||
|
error={fileError || undefined} //now passes local error state
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuditLoggingCertificateUploadSetting;
|
||||||
@@ -29,6 +29,9 @@ type State = {
|
|||||||
export default class FileUploadSetting extends React.PureComponent<Props, State> {
|
export default class FileUploadSetting extends React.PureComponent<Props, State> {
|
||||||
fileInputRef = React.createRef<HTMLInputElement>();
|
fileInputRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
// Helps prevent setting state after component is unmounted, for usage when this component is wrapped by a custom setting
|
||||||
|
isMounted = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -40,6 +43,14 @@ export default class FileUploadSetting extends React.PureComponent<Props, State>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.isMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.isMounted = false;
|
||||||
|
}
|
||||||
|
|
||||||
handleChooseClick = () => {
|
handleChooseClick = () => {
|
||||||
this.fileInputRef.current?.click();
|
this.fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
@@ -58,9 +69,11 @@ export default class FileUploadSetting extends React.PureComponent<Props, State>
|
|||||||
const file = this.fileInputRef.current?.files?.[0];
|
const file = this.fileInputRef.current?.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
this.props.onSubmit(this.props.id, file, (error) => {
|
this.props.onSubmit(this.props.id, file, (error) => {
|
||||||
this.setState({uploading: false});
|
if (this.isMounted) {
|
||||||
if (error && this.fileInputRef.current) {
|
this.setState({uploading: false});
|
||||||
Utils.clearFileInput(this.fileInputRef.current);
|
if (error && this.fileInputRef.current) {
|
||||||
|
Utils.clearFileInput(this.fileInputRef.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type AdminDefinitionSettingCustom = Omit<AdminDefinitionSettingBase, 'label'> &
|
|||||||
key: string;
|
key: string;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
component: Component;
|
component: Component;
|
||||||
label?: string;
|
label?: string | MessageDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminDefinitionSettingBase = {
|
type AdminDefinitionSettingBase = {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useEffect, useState, useCallback} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
|
import {getInstallation} from 'actions/cloud';
|
||||||
|
|
||||||
|
export default function useGetCloudInstallationStatus(poll: boolean = false) {
|
||||||
|
const [status, setStatus] = useState<string>('');
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const license = useSelector(getLicense);
|
||||||
|
|
||||||
|
const fetchStatus = useCallback(async () => {
|
||||||
|
if (license.Cloud === 'true') {
|
||||||
|
const result = await dispatch(getInstallation());
|
||||||
|
if (result.data) {
|
||||||
|
setStatus(result.data.state);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setStatus('stable');
|
||||||
|
}
|
||||||
|
}, [dispatch, license]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStatus();
|
||||||
|
if (poll && license.Cloud === 'true') {
|
||||||
|
const interval = setInterval(fetchStatus, 5000); // Poll every 5 seconds
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [fetchStatus, poll, license]);
|
||||||
|
|
||||||
|
return {status, refetchStatus: fetchStatus};
|
||||||
|
}
|
||||||
@@ -257,6 +257,13 @@
|
|||||||
"admin.advance.metrics": "Performance Monitoring",
|
"admin.advance.metrics": "Performance Monitoring",
|
||||||
"admin.announcement_banner_feature_discovery.copy": "Create announcement banners to notify all members of important information.",
|
"admin.announcement_banner_feature_discovery.copy": "Create announcement banners to notify all members of important information.",
|
||||||
"admin.announcement_banner_feature_discovery.title": "Create custom announcement banners with Mattermost Professional",
|
"admin.announcement_banner_feature_discovery.title": "Create custom announcement banners with Mattermost Professional",
|
||||||
|
"admin.audit_logging_experimental.certificate.help_text": "The certificate file used for audit logging encryption.",
|
||||||
|
"admin.audit_logging_experimental.certificate.remove_button": "Remove Certificate",
|
||||||
|
"admin.audit_logging_experimental.certificate.remove_help_text": "Remove the certificate used for audit logging encryption.",
|
||||||
|
"admin.audit_logging_experimental.certificate.removing": "Removing Certificate...",
|
||||||
|
"admin.audit_logging_experimental.certificate.title": "Certificate",
|
||||||
|
"admin.audit_logging_experimental.certificate.tooltip": "A previous update is still in progress. Please wait.",
|
||||||
|
"admin.audit_logging_experimental.certificate.uploading": "Uploading Certificate...",
|
||||||
"admin.audit_logging_experimental.file_compress.help_text": "Choose whether enable or disable file compression.",
|
"admin.audit_logging_experimental.file_compress.help_text": "Choose whether enable or disable file compression.",
|
||||||
"admin.audit_logging_experimental.file_compress.title": "File Compression",
|
"admin.audit_logging_experimental.file_compress.title": "File Compression",
|
||||||
"admin.audit_logging_experimental.file_enabled.help_text": "Choose whether audit logs are written locally to a file or not.",
|
"admin.audit_logging_experimental.file_enabled.help_text": "Choose whether audit logs are written locally to a file or not.",
|
||||||
|
|||||||
@@ -319,6 +319,21 @@ export function uploadIdpSamlCertificate(fileData: File) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function uploadAuditCertificate(fileData: File) {
|
||||||
|
return bindClientFunc({
|
||||||
|
clientFunc: Client4.uploadAuditLogCertificate,
|
||||||
|
params: [
|
||||||
|
fileData,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAuditCertificate() {
|
||||||
|
return bindClientFunc({
|
||||||
|
clientFunc: Client4.removeAuditLogCertificate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function removePublicSamlCertificate() {
|
export function removePublicSamlCertificate() {
|
||||||
return bindClientFunc({
|
return bindClientFunc({
|
||||||
clientFunc: Client4.deletePublicSamlCertificate,
|
clientFunc: Client4.deletePublicSamlCertificate,
|
||||||
|
|||||||
@@ -3385,6 +3385,26 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uploadAuditLogCertificate = (fileData: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('certificate', fileData);
|
||||||
|
|
||||||
|
return this.doFetch<StatusOK>(
|
||||||
|
`${this.getBaseRoute()}/audit_logs/certificate`,
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
removeAuditLogCertificate = () => {
|
||||||
|
return this.doFetch<StatusOK>(
|
||||||
|
`${this.getBaseRoute()}/audit_logs/certificate`,
|
||||||
|
{method: 'delete'},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
deletePublicSamlCertificate = () => {
|
deletePublicSamlCertificate = () => {
|
||||||
return this.doFetch<StatusOK>(
|
return this.doFetch<StatusOK>(
|
||||||
`${this.getBaseRoute()}/saml/certificate/public`,
|
`${this.getBaseRoute()}/saml/certificate/public`,
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user