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)/scheduled_post.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)/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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
- name: metrics
|
||||
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:
|
||||
- name: Overview
|
||||
tags:
|
||||
@@ -618,6 +620,7 @@ x-tagGroups:
|
||||
- reports
|
||||
- custom profile attributes
|
||||
- metrics
|
||||
- audit_logs
|
||||
servers:
|
||||
- url: http://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'
|
||||
CustomProfileAttributesField *mux.Router // 'api/v4/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
|
||||
CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
|
||||
|
||||
AuditLogs *mux.Router // 'api/v4/audit_logs'
|
||||
}
|
||||
|
||||
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.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
|
||||
|
||||
api.BaseRoutes.AuditLogs = api.BaseRoutes.APIRoot.PathPrefix("/audit_logs").Subrouter()
|
||||
|
||||
api.InitUser()
|
||||
api.InitBot()
|
||||
api.InitTeam()
|
||||
@@ -349,6 +353,7 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.InitClientPerformanceMetrics()
|
||||
api.InitScheduledPost()
|
||||
api.InitCustomProfileAttributes()
|
||||
api.InitAuditLogging()
|
||||
|
||||
// If we allow testing then listen for manual testing URL hits
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
@@ -28,6 +30,10 @@ var (
|
||||
LevelCLI = mlog.LvlAuditCLI
|
||||
)
|
||||
|
||||
const (
|
||||
AuditCertificateFilename = "audit_certificate.pem"
|
||||
)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -156,3 +162,66 @@ func (s *Server) onAuditTargetQueueFull(qname string, maxQSize int) bool {
|
||||
func (s *Server) onAuditError(err error) {
|
||||
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
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
@@ -37,4 +39,7 @@ type CloudInterface interface {
|
||||
ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
|
||||
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
|
||||
GetInstallation(userID string) (*model.Installation, error)
|
||||
|
||||
RemoveAuditLoggingCert(userID string) error
|
||||
CreateAuditLoggingCert(userID string, fileData *multipart.FileHeader) error
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
multipart "mime/multipart"
|
||||
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -92,6 +94,24 @@ func (_m *CloudInterface) CheckCWSConnection(userId string) error {
|
||||
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
|
||||
func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) {
|
||||
ret := _m.Called(userID, userCount)
|
||||
@@ -465,6 +485,24 @@ func (_m *CloudInterface) InvalidateCaches() error {
|
||||
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
|
||||
func (_m *CloudInterface) SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error {
|
||||
ret := _m.Called(userID, req)
|
||||
|
||||
@@ -59,10 +59,18 @@
|
||||
"id": "api.acknowledgement.save.archived_channel.app_error",
|
||||
"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",
|
||||
"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",
|
||||
"translation": "No file under 'certificate' in request."
|
||||
@@ -95,6 +103,10 @@
|
||||
"id": "api.admin.ldap.not_available.app_error",
|
||||
"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",
|
||||
"translation": "An error occurred while deleting the certificate."
|
||||
|
||||
@@ -1538,6 +1538,7 @@ type ExperimentalAuditSettings struct {
|
||||
FileCompress *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||
FileMaxQueueSize *int `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||
AdvancedLoggingJSON json.RawMessage `access:"experimental_features"`
|
||||
Certificate *string `access:"experimental_features"` // telemetry: none
|
||||
}
|
||||
|
||||
func (s *ExperimentalAuditSettings) SetDefaults() {
|
||||
@@ -1572,6 +1573,10 @@ func (s *ExperimentalAuditSettings) SetDefaults() {
|
||||
if utils.IsEmptyJSON(s.AdvancedLoggingJSON) {
|
||||
s.AdvancedLoggingJSON = []byte("{}")
|
||||
}
|
||||
|
||||
if s.Certificate == nil {
|
||||
s.Certificate = NewPointer("")
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const {data, error: err} = await dispatch(AdminActions.removePublicSamlCertificate());
|
||||
if (data && success) {
|
||||
|
||||
@@ -42,6 +42,7 @@ import {ID_PATH_PATTERN} from 'utils/path';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
|
||||
import * as DefinitionConstants from './admin_definition_constants';
|
||||
import AuditLoggingCertificateUploadSetting from './audit_logging';
|
||||
import Audits from './audits';
|
||||
import {searchableStrings as auditSearchableStrings} from './audits/audits';
|
||||
import BillingHistory, {searchableStrings as billingHistorySearchableStrings} from './billing/billing_history';
|
||||
@@ -6715,6 +6716,15 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
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> {
|
||||
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) {
|
||||
super(props);
|
||||
|
||||
@@ -40,6 +43,14 @@ export default class FileUploadSetting extends React.PureComponent<Props, State>
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
handleChooseClick = () => {
|
||||
this.fileInputRef.current?.click();
|
||||
};
|
||||
@@ -58,9 +69,11 @@ export default class FileUploadSetting extends React.PureComponent<Props, State>
|
||||
const file = this.fileInputRef.current?.files?.[0];
|
||||
if (file) {
|
||||
this.props.onSubmit(this.props.id, file, (error) => {
|
||||
this.setState({uploading: false});
|
||||
if (error && this.fileInputRef.current) {
|
||||
Utils.clearFileInput(this.fileInputRef.current);
|
||||
if (this.isMounted) {
|
||||
this.setState({uploading: false});
|
||||
if (error && this.fileInputRef.current) {
|
||||
Utils.clearFileInput(this.fileInputRef.current);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type AdminDefinitionSettingCustom = Omit<AdminDefinitionSettingBase, 'label'> &
|
||||
key: string;
|
||||
showTitle?: boolean;
|
||||
component: Component;
|
||||
label?: string;
|
||||
label?: string | MessageDescriptor;
|
||||
}
|
||||
|
||||
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.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.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.title": "File Compression",
|
||||
"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() {
|
||||
return bindClientFunc({
|
||||
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 = () => {
|
||||
return this.doFetch<StatusOK>(
|
||||
`${this.getBaseRoute()}/saml/certificate/public`,
|
||||
|
||||
Ссылка в новой задаче
Block a user