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>
Этот коммит содержится в:
Nick Misasi
2025-04-16 09:34:18 -04:00
коммит произвёл GitHub
родитель e113b3cfc8
Коммит 495a49b896
19 изменённых файлов: 567 добавлений и 5 удалений

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 users `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 Обычный файл
Просмотреть файл

@@ -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')),
},
],
},
},

Просмотреть файл

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