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>
Этот коммит содержится в:
@@ -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