summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorJoramWilander <jwawilander@gmail.com>2016-02-02 16:49:27 -0500
committerJoramWilander <jwawilander@gmail.com>2016-02-02 16:49:27 -0500
commitd153d661db7d4349d69824d318aa9ad571970606 (patch)
tree002c035bbd99d8e2be4a7c7ebb18b413de55ad6e /web
parentf28486c4553f7f4bccf7bf69153c2f12699705f9 (diff)
downloadchat-d153d661db7d4349d69824d318aa9ad571970606.tar.gz
chat-d153d661db7d4349d69824d318aa9ad571970606.tar.bz2
chat-d153d661db7d4349d69824d318aa9ad571970606.zip
Add basic server audit tab to system console for EE
Diffstat (limited to 'web')
-rw-r--r--web/react/components/access_history_modal.jsx548
-rw-r--r--web/react/components/admin_console/admin_controller.jsx3
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx19
-rw-r--r--web/react/components/admin_console/audits.jsx94
-rw-r--r--web/react/components/audit_table.jsx571
-rw-r--r--web/react/stores/admin_store.jsx32
-rw-r--r--web/react/utils/async_client.jsx26
-rw-r--r--web/react/utils/client.jsx14
-rw-r--r--web/react/utils/constants.jsx1
-rw-r--r--web/static/i18n/en.json100
10 files changed, 824 insertions, 584 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 6319b5681..98b1d7cc1 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -1,204 +1,24 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
var Modal = ReactBootstrap.Modal;
+import LoadingScreen from './loading_screen.jsx';
+import AuditTable from './audit_table.jsx';
+
import UserStore from '../stores/user_store.jsx';
-import ChannelStore from '../stores/channel_store.jsx';
+
import * as AsyncClient from '../utils/async_client.jsx';
-import LoadingScreen from './loading_screen.jsx';
import * as Utils from '../utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
-
-const holders = defineMessages({
- sessionRevoked: {
- id: 'access_history.sessionRevoked',
- defaultMessage: 'The session with id {sessionId} was revoked'
- },
- channelCreated: {
- id: 'access_history.channelCreated',
- defaultMessage: 'Created the {channelName} channel/group'
- },
- establishedDM: {
- id: 'access_history.establishedDM',
- defaultMessage: 'Established a direct message channel with {username}'
- },
- nameUpdated: {
- id: 'access_history.nameUpdated',
- defaultMessage: 'Updated the {channelName} channel/group name'
- },
- headerUpdated: {
- id: 'access_history.headerUpdated',
- defaultMessage: 'Updated the {channelName} channel/group header'
- },
- channelDeleted: {
- id: 'access_history.channelDeleted',
- defaultMessage: 'Deleted the channel/group with the URL {url}'
- },
- userAdded: {
- id: 'access_history.userAdded',
- defaultMessage: 'Added {username} to the {channelName} channel/group'
- },
- userRemoved: {
- id: 'access_history.userRemoved',
- defaultMessage: 'Removed {username} to the {channelName} channel/group'
- },
- attemptedRegisterApp: {
- id: 'access_history.attemptedRegisterApp',
- defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
- },
- attemptedAllowOAuthAccess: {
- id: 'access_history.attemptedAllowOAuthAccess',
- defaultMessage: 'Attempted to allow a new OAuth service access'
- },
- successfullOAuthAccess: {
- id: 'access_history.successfullOAuthAccess',
- defaultMessage: 'Successfully gave a new OAuth service access'
- },
- failedOAuthAccess: {
- id: 'access_history.failedOAuthAccess',
- defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
- },
- attemptedOAuthToken: {
- id: 'access_history.attemptedOAuthToken',
- defaultMessage: 'Attempted to get an OAuth access token'
- },
- successfullOAuthToken: {
- id: 'access_history.successfullOAuthToken',
- defaultMessage: 'Successfully added a new OAuth service'
- },
- oauthTokenFailed: {
- id: 'access_history.oauthTokenFailed',
- defaultMessage: 'Failed to get an OAuth access token - {token}'
- },
- attemptedLogin: {
- id: 'access_history.attemptedLogin',
- defaultMessage: 'Attempted to login'
- },
- successfullLogin: {
- id: 'access_history.successfullLogin',
- defaultMessage: 'Successfully logged in'
- },
- failedLogin: {
- id: 'access_history.failedLogin',
- defaultMessage: 'FAILED login attempt'
- },
- updatePicture: {
- id: 'access_history.updatePicture',
- defaultMessage: 'Updated your profile picture'
- },
- updateGeneral: {
- id: 'access_history.updateGeneral',
- defaultMessage: 'Updated the general settings of your account'
- },
- attemptedPassword: {
- id: 'access_history.attemptedPassword',
- defaultMessage: 'Attempted to change password'
- },
- successfullPassword: {
- id: 'access_history.successfullPassword',
- defaultMessage: 'Successfully changed password'
- },
- failedPassword: {
- id: 'access_history.failedPassword',
- defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
- },
- updatedRol: {
- id: 'access_history.updatedRol',
- defaultMessage: 'Updated user role(s) to '
- },
- member: {
- id: 'access_history.member',
- defaultMessage: 'member'
- },
- accountActive: {
- id: 'access_history.accountActive',
- defaultMessage: 'Account made active'
- },
- accountInactive: {
- id: 'access_history.accountInactive',
- defaultMessage: 'Account made inactive'
- },
- by: {
- id: 'access_history.by',
- defaultMessage: ' by {username}'
- },
- byAdmin: {
- id: 'access_history.byAdmin',
- defaultMessage: ' by an admin'
- },
- sentEmail: {
- id: 'access_history.sentEmail',
- defaultMessage: 'Sent an email to {email} to reset your password'
- },
- attemptedReset: {
- id: 'access_history.attemptedReset',
- defaultMessage: 'Attempted to reset password'
- },
- successfullReset: {
- id: 'access_history.successfullReset',
- defaultMessage: 'Successfully reset password'
- },
- updateGlobalNotifications: {
- id: 'access_history.updateGlobalNotifications',
- defaultMessage: 'Updated your global notification settings'
- },
- attemptedWebhookCreate: {
- id: 'access_history.attemptedWebhookCreate',
- defaultMessage: 'Attempted to create a webhook'
- },
- succcessfullWebhookCreate: {
- id: 'access_history.successfullWebhookCreate',
- defaultMessage: 'Successfully created a webhook'
- },
- failedWebhookCreate: {
- id: 'access_history.failedWebhookCreate',
- defaultMessage: 'Failed to create a webhook - bad channel permissions'
- },
- attemptedWebhookDelete: {
- id: 'access_history.attemptedWebhookDelete',
- defaultMessage: 'Attempted to delete a webhook'
- },
- successfullWebhookDelete: {
- id: 'access_history.successfullWebhookDelete',
- defaultMessage: 'Successfully deleted a webhook'
- },
- failedWebhookDelete: {
- id: 'access_history.failedWebhookDelete',
- defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
- },
- logout: {
- id: 'access_history.logout',
- defaultMessage: 'Logged out of your account'
- },
- verified: {
- id: 'access_history.verified',
- defaultMessage: 'Sucessfully verified your email address'
- },
- revokedAll: {
- id: 'access_history.revokedAll',
- defaultMessage: 'Revoked all current sessions for the team'
- },
- loginAttempt: {
- id: 'access_history.loginAttempt',
- defaultMessage: ' (Login attempt)'
- },
- loginFailure: {
- id: 'access_history.loginFailure',
- defaultMessage: ' (Login failure)'
- }
-});
+import {intlShape, injectIntl, FormattedMessage} from 'mm-intl';
class AccessHistoryModal extends React.Component {
constructor(props) {
super(props);
this.onAuditChange = this.onAuditChange.bind(this);
- this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.onShow = this.onShow.bind(this);
this.onHide = this.onHide.bind(this);
- this.formatAuditInfo = this.formatAuditInfo.bind(this);
- this.handleRevokedSession = this.handleRevokedSession.bind(this);
const state = this.getStateFromStoresForAudits();
state.moreInfo = [];
@@ -245,359 +65,17 @@ class AccessHistoryModal extends React.Component {
this.setState(newState);
}
}
- handleMoreInfo(index) {
- var newMoreInfo = this.state.moreInfo;
- newMoreInfo[index] = true;
- this.setState({moreInfo: newMoreInfo});
- }
- handleRevokedSession(sessionId) {
- return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
- }
- formatAuditInfo(currentAudit) {
- const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
-
- const {formatMessage} = this.props.intl;
- let currentAuditDesc = '';
-
- if (currentActionURL.indexOf('/channels') === 0) {
- const channelInfo = currentAudit.extra_info.split(' ');
- const channelNameField = channelInfo[0].split('=');
-
- let channelURL = '';
- let channelObj;
- let channelName = '';
- if (channelNameField.indexOf('name') >= 0) {
- channelURL = channelNameField[channelNameField.indexOf('name') + 1];
- channelObj = ChannelStore.getByName(channelURL);
- if (channelObj) {
- channelName = channelObj.display_name;
- } else {
- channelName = channelURL;
- }
- }
-
- switch (currentActionURL) {
- case '/channels/create':
- currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
- break;
- case '/channels/create_direct':
- currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
- break;
- case '/channels/update':
- currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
- break;
- case '/channels/update_desc': // support the old path
- case '/channels/update_header':
- currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
- break;
- default: {
- let userIdField = [];
- let userId = '';
- let username = '';
-
- if (channelInfo[1]) {
- userIdField = channelInfo[1].split('=');
-
- if (userIdField.indexOf('user_id') >= 0) {
- userId = userIdField[userIdField.indexOf('user_id') + 1];
- username = UserStore.getProfile(userId).username;
- }
- }
-
- if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
- } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
- } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
- currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
- }
-
- break;
- }
- }
- } else if (currentActionURL.indexOf('/oauth') === 0) {
- const oauthInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/oauth/register': {
- const clientIdField = oauthInfo[0].split('=');
-
- if (clientIdField[0] === 'client_id') {
- currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
- }
-
- break;
- }
- case '/oauth/allow':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
- } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
- currentAuditDesc = formatMessage(holders.failedOAuthAccess);
- }
-
- break;
- case '/oauth/access_token':
- if (oauthInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
- } else if (oauthInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullOAuthToken);
- } else {
- const oauthTokenFailure = oauthInfo[0].split('-');
-
- if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
- currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
- }
- }
-
- break;
- default:
- break;
- }
- } else if (currentActionURL.indexOf('/users') === 0) {
- const userInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/users/login':
- if (userInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedLogin);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullLogin);
- } else if (userInfo[0]) {
- currentAuditDesc = formatMessage(holders.failedLogin);
- }
-
- break;
- case '/users/revoke_session':
- currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
- break;
- case '/users/newimage':
- currentAuditDesc = formatMessage(holders.updatePicture);
- break;
- case '/users/update':
- currentAuditDesc = formatMessage(holders.updateGeneral);
- break;
- case '/users/newpassword':
- if (userInfo[0] === 'attempted') {
- currentAuditDesc = formatMessage(holders.attemptedPassword);
- } else if (userInfo[0] === 'completed') {
- currentAuditDesc = formatMessage(holders.successfullPassword);
- } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
- currentAuditDesc = formatMessage(holders.failedPassword);
- }
-
- break;
- case '/users/update_roles': {
- const userRoles = userInfo[0].split('=')[1];
-
- currentAuditDesc = formatMessage(holders.updatedRol);
- if (userRoles.trim()) {
- currentAuditDesc += userRoles;
- } else {
- currentAuditDesc += formatMessage(holders.member);
- }
-
- break;
- }
- case '/users/update_active': {
- const updateType = userInfo[0].split('=')[0];
- const updateField = userInfo[0].split('=')[1];
-
- /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
- if (updateType === 'active') {
- if (updateField === 'true') {
- currentAuditDesc = formatMessage(holders.accountActive);
- } else if (updateField === 'false') {
- currentAuditDesc = formatMessage(holders.accountInactive);
- }
-
- const actingUserInfo = userInfo[1].split('=');
- if (actingUserInfo[0] === 'session_user') {
- const actingUser = UserStore.getProfile(actingUserInfo[1]);
- const currentUser = UserStore.getCurrentUser();
- if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
- currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
- } else if (currentUser && actingUser) {
- currentAuditDesc += formatMessage(holders.byAdmin);
- }
- }
- } else if (updateType === 'session_id') {
- currentAuditDesc = this.handleRevokedSession(updateField);
- }
-
- break;
- }
- case '/users/send_password_reset':
- currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
- break;
- case '/users/reset_password':
- if (userInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedReset);
- } else if (userInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullReset);
- }
-
- break;
- case '/users/update_notify':
- currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
- break;
- default:
- break;
- }
- } else if (currentActionURL.indexOf('/hooks') === 0) {
- const webhookInfo = currentAudit.extra_info.split(' ');
-
- switch (currentActionURL) {
- case '/hooks/incoming/create':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
- } else if (webhookInfo[0] === 'fail - bad channel permissions') {
- currentAuditDesc = formatMessage(holders.failedWebhookCreate);
- }
-
- break;
- case '/hooks/incoming/delete':
- if (webhookInfo[0] === 'attempt') {
- currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
- } else if (webhookInfo[0] === 'success') {
- currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
- } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
- currentAuditDesc = formatMessage(holders.failedWebhookDelete);
- }
-
- break;
- default:
- break;
- }
- } else {
- switch (currentActionURL) {
- case '/logout':
- currentAuditDesc = formatMessage(holders.logout);
- break;
- case '/verify_email':
- currentAuditDesc = formatMessage(holders.verified);
- break;
- default:
- break;
- }
- }
-
- /* If all else fails... */
- if (!currentAuditDesc) {
- /* Currently not called anywhere */
- if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
- currentAuditDesc = formatMessage(holders.revokedAll);
- } else {
- let currentActionDesc = '';
- if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
- currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
- currentActionDesc = Utils.toTitleCase(currentActionDesc);
- }
-
- let currentExtraInfoDesc = '';
- if (currentAudit.extra_info) {
- currentExtraInfoDesc = currentAudit.extra_info;
-
- if (currentExtraInfoDesc.indexOf('=') !== -1) {
- currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
- }
- }
- currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
- }
- }
-
- const currentDate = new Date(currentAudit.create_at);
- const currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' +
- currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc;
- return currentAuditInfo;
- }
render() {
- var accessList = [];
-
- const {formatMessage} = this.props.intl;
- for (var i = 0; i < this.state.audits.length; i++) {
- const currentAudit = this.state.audits[i];
- const currentAuditInfo = this.formatAuditInfo(currentAudit);
-
- var moreInfo = (
- <a
- href='#'
- className='theme'
- onClick={this.handleMoreInfo.bind(this, i)}
- >
- <FormattedMessage
- id='access_history.moreInfo'
- defaultMessage='More info'
- />
- </a>
- );
-
- if (this.state.moreInfo[i]) {
- if (!currentAudit.session_id) {
- currentAudit.session_id = 'N/A';
-
- if (currentAudit.action.search('/users/login') >= 0) {
- if (currentAudit.extra_info === 'attempt') {
- currentAudit.session_id += formatMessage(holders.loginAttempt);
- } else {
- currentAudit.session_id += formatMessage(holders.loginFailure);
- }
- }
- }
-
- moreInfo = (
- <div>
- <div>
- <FormattedMessage
- id='access_history.ip'
- defaultMessage='IP: {ip}'
- values={{
- ip: currentAudit.ip_address
- }}
- />
- </div>
- <div>
- <FormattedMessage
- id='access_history.session'
- defaultMessage='Session ID: {id}'
- values={{
- id: currentAudit.session_id
- }}
- />
- </div>
- </div>
- );
- }
-
- var divider = null;
- if (i < this.state.audits.length - 1) {
- divider = (<div className='divider-light'></div>);
- }
-
- accessList[i] = (
- <div
- key={'accessHistoryEntryKey' + i}
- className='access-history__table'
- >
- <div className='access__report'>
- <div className='report__time'>{currentAuditInfo}</div>
- <div className='report__info'>
- {moreInfo}
- </div>
- {divider}
- </div>
- </div>
- );
- }
-
var content;
if (this.state.audits.loading) {
content = (<LoadingScreen />);
} else {
- content = (<form role='form'>{accessList}</form>);
+ content = (
+ <AuditTable
+ audits={this.state.audits}
+ moreInfo={this.state.moreInfo}
+ />
+ );
}
return (
@@ -628,4 +106,4 @@ AccessHistoryModal.propTypes = {
onHide: React.PropTypes.func.isRequired
};
-export default injectIntl(AccessHistoryModal); \ No newline at end of file
+export default injectIntl(AccessHistoryModal);
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index efd163017..360ae3ef3 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -11,6 +11,7 @@ import * as Utils from '../../utils/utils.jsx';
import EmailSettingsTab from './email_settings.jsx';
import LogSettingsTab from './log_settings.jsx';
import LogsTab from './logs.jsx';
+import AuditsTab from './audits.jsx';
import FileSettingsTab from './image_settings.jsx';
import PrivacySettingsTab from './privacy_settings.jsx';
import RateSettingsTab from './rate_settings.jsx';
@@ -138,6 +139,8 @@ export default class AdminController extends React.Component {
tab = <LogSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
+ } else if (this.state.selected === 'audits') {
+ tab = <AuditsTab />;
} else if (this.state.selected === 'image_settings') {
tab = <FileSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'privacy_settings') {
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index d6bae1feb..642bfe9d7 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -214,6 +214,24 @@ export default class AdminSidebar extends React.Component {
);
}
+ let audits;
+ if (global.window.mm_license.IsLicensed === 'true') {
+ audits = (
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('audits')}
+ onClick={this.handleClick.bind(this, 'audits', null)}
+ >
+ <FormattedMessage
+ id='admin.sidebar.audits'
+ defaultMessage='Audits'
+ />
+ </a>
+ </li>
+ );
+ }
+
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -448,6 +466,7 @@ export default class AdminSidebar extends React.Component {
/>
</a>
</li>
+ {audits}
</ul>
</li>
</ul>
diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx
new file mode 100644
index 000000000..866539b3d
--- /dev/null
+++ b/web/react/components/admin_console/audits.jsx
@@ -0,0 +1,94 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LoadingScreen from '../loading_screen.jsx';
+import AuditTable from '../audit_table.jsx';
+
+import AdminStore from '../../stores/admin_store.jsx';
+
+import * as AsyncClient from '../../utils/async_client.jsx';
+
+import {FormattedMessage} from 'mm-intl';
+
+export default class Audits extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onAuditListenerChange = this.onAuditListenerChange.bind(this);
+ this.reload = this.reload.bind(this);
+
+ this.state = {
+ audits: AdminStore.getAudits()
+ };
+ }
+
+ componentDidMount() {
+ AdminStore.addAuditChangeListener(this.onAuditListenerChange);
+ AsyncClient.getServerAudits();
+ }
+
+ componentWillUnmount() {
+ AdminStore.removeAuditChangeListener(this.onAuditListenerChange);
+ }
+
+ onAuditListenerChange() {
+ this.setState({
+ audits: AdminStore.getAudits()
+ });
+ }
+
+ reload() {
+ AdminStore.saveAudits(null);
+ this.setState({
+ audits: null
+ });
+
+ AsyncClient.getServerAudits();
+ }
+
+ render() {
+ var content = null;
+
+ if (global.window.mm_license.IsLicensed !== 'true') {
+ return <div/>;
+ }
+
+ if (this.state.audits === null) {
+ content = <LoadingScreen />;
+ } else {
+ content = (
+ <div style={{margin: '10px'}}>
+ <AuditTable
+ audits={this.state.audits}
+ oneLine={true}
+ showUserId={true}
+ />
+ </div>
+ );
+ }
+
+ return (
+ <div className='panel'>
+ <h3>
+ <FormattedMessage
+ id='admin.audits.title'
+ defaultMessage='Server Audits'
+ />
+ </h3>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ onClick={this.reload}
+ >
+ <FormattedMessage
+ id='admin.audits.reload'
+ defaultMessage='Reload'
+ />
+ </button>
+ <div className='log__panel'>
+ {content}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/audit_table.jsx b/web/react/components/audit_table.jsx
new file mode 100644
index 000000000..cdca7e8d6
--- /dev/null
+++ b/web/react/components/audit_table.jsx
@@ -0,0 +1,571 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import UserStore from '../stores/user_store.jsx';
+import ChannelStore from '../stores/channel_store.jsx';
+import * as Utils from '../utils/utils.jsx';
+
+import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ sessionRevoked: {
+ id: 'audit_table.sessionRevoked',
+ defaultMessage: 'The session with id {sessionId} was revoked'
+ },
+ channelCreated: {
+ id: 'audit_table.channelCreated',
+ defaultMessage: 'Created the {channelName} channel/group'
+ },
+ establishedDM: {
+ id: 'audit_table.establishedDM',
+ defaultMessage: 'Established a direct message channel with {username}'
+ },
+ nameUpdated: {
+ id: 'audit_table.nameUpdated',
+ defaultMessage: 'Updated the {channelName} channel/group name'
+ },
+ headerUpdated: {
+ id: 'audit_table.headerUpdated',
+ defaultMessage: 'Updated the {channelName} channel/group header'
+ },
+ channelDeleted: {
+ id: 'audit_table.channelDeleted',
+ defaultMessage: 'Deleted the channel/group with the URL {url}'
+ },
+ userAdded: {
+ id: 'audit_table.userAdded',
+ defaultMessage: 'Added {username} to the {channelName} channel/group'
+ },
+ userRemoved: {
+ id: 'audit_table.userRemoved',
+ defaultMessage: 'Removed {username} to the {channelName} channel/group'
+ },
+ attemptedRegisterApp: {
+ id: 'audit_table.attemptedRegisterApp',
+ defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
+ },
+ attemptedAllowOAuthAccess: {
+ id: 'audit_table.attemptedAllowOAuthAccess',
+ defaultMessage: 'Attempted to allow a new OAuth service access'
+ },
+ successfullOAuthAccess: {
+ id: 'audit_table.successfullOAuthAccess',
+ defaultMessage: 'Successfully gave a new OAuth service access'
+ },
+ failedOAuthAccess: {
+ id: 'audit_table.failedOAuthAccess',
+ defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
+ },
+ attemptedOAuthToken: {
+ id: 'audit_table.attemptedOAuthToken',
+ defaultMessage: 'Attempted to get an OAuth access token'
+ },
+ successfullOAuthToken: {
+ id: 'audit_table.successfullOAuthToken',
+ defaultMessage: 'Successfully added a new OAuth service'
+ },
+ oauthTokenFailed: {
+ id: 'audit_table.oauthTokenFailed',
+ defaultMessage: 'Failed to get an OAuth access token - {token}'
+ },
+ attemptedLogin: {
+ id: 'audit_table.attemptedLogin',
+ defaultMessage: 'Attempted to login'
+ },
+ successfullLogin: {
+ id: 'audit_table.successfullLogin',
+ defaultMessage: 'Successfully logged in'
+ },
+ failedLogin: {
+ id: 'audit_table.failedLogin',
+ defaultMessage: 'FAILED login attempt'
+ },
+ updatePicture: {
+ id: 'audit_table.updatePicture',
+ defaultMessage: 'Updated your profile picture'
+ },
+ updateGeneral: {
+ id: 'audit_table.updateGeneral',
+ defaultMessage: 'Updated the general settings of your account'
+ },
+ attemptedPassword: {
+ id: 'audit_table.attemptedPassword',
+ defaultMessage: 'Attempted to change password'
+ },
+ successfullPassword: {
+ id: 'audit_table.successfullPassword',
+ defaultMessage: 'Successfully changed password'
+ },
+ failedPassword: {
+ id: 'audit_table.failedPassword',
+ defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
+ },
+ updatedRol: {
+ id: 'audit_table.updatedRol',
+ defaultMessage: 'Updated user role(s) to '
+ },
+ member: {
+ id: 'audit_table.member',
+ defaultMessage: 'member'
+ },
+ accountActive: {
+ id: 'audit_table.accountActive',
+ defaultMessage: 'Account made active'
+ },
+ accountInactive: {
+ id: 'audit_table.accountInactive',
+ defaultMessage: 'Account made inactive'
+ },
+ by: {
+ id: 'audit_table.by',
+ defaultMessage: ' by {username}'
+ },
+ byAdmin: {
+ id: 'audit_table.byAdmin',
+ defaultMessage: ' by an admin'
+ },
+ sentEmail: {
+ id: 'audit_table.sentEmail',
+ defaultMessage: 'Sent an email to {email} to reset your password'
+ },
+ attemptedReset: {
+ id: 'audit_table.attemptedReset',
+ defaultMessage: 'Attempted to reset password'
+ },
+ successfullReset: {
+ id: 'audit_table.successfullReset',
+ defaultMessage: 'Successfully reset password'
+ },
+ updateGlobalNotifications: {
+ id: 'audit_table.updateGlobalNotifications',
+ defaultMessage: 'Updated your global notification settings'
+ },
+ attemptedWebhookCreate: {
+ id: 'audit_table.attemptedWebhookCreate',
+ defaultMessage: 'Attempted to create a webhook'
+ },
+ succcessfullWebhookCreate: {
+ id: 'audit_table.successfullWebhookCreate',
+ defaultMessage: 'Successfully created a webhook'
+ },
+ failedWebhookCreate: {
+ id: 'audit_table.failedWebhookCreate',
+ defaultMessage: 'Failed to create a webhook - bad channel permissions'
+ },
+ attemptedWebhookDelete: {
+ id: 'audit_table.attemptedWebhookDelete',
+ defaultMessage: 'Attempted to delete a webhook'
+ },
+ successfullWebhookDelete: {
+ id: 'audit_table.successfullWebhookDelete',
+ defaultMessage: 'Successfully deleted a webhook'
+ },
+ failedWebhookDelete: {
+ id: 'audit_table.failedWebhookDelete',
+ defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
+ },
+ logout: {
+ id: 'audit_table.logout',
+ defaultMessage: 'Logged out of your account'
+ },
+ verified: {
+ id: 'audit_table.verified',
+ defaultMessage: 'Sucessfully verified your email address'
+ },
+ revokedAll: {
+ id: 'audit_table.revokedAll',
+ defaultMessage: 'Revoked all current sessions for the team'
+ },
+ loginAttempt: {
+ id: 'audit_table.loginAttempt',
+ defaultMessage: ' (Login attempt)'
+ },
+ loginFailure: {
+ id: 'audit_table.loginFailure',
+ defaultMessage: ' (Login failure)'
+ },
+ userId: {
+ id: 'audit_table.userId',
+ defaultMessage: 'User ID'
+ }
+});
+
+class AuditTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMoreInfo = this.handleMoreInfo.bind(this);
+ this.formatAuditInfo = this.formatAuditInfo.bind(this);
+ this.handleRevokedSession = this.handleRevokedSession.bind(this);
+
+ this.state = {moreInfo: []};
+ }
+ handleMoreInfo(index) {
+ var newMoreInfo = this.state.moreInfo;
+ newMoreInfo[index] = true;
+ this.setState({moreInfo: newMoreInfo});
+ }
+ handleRevokedSession(sessionId) {
+ return this.props.intl.formatMessage(holders.sessionRevoked, {sessionId: sessionId});
+ }
+ formatAuditInfo(currentAudit) {
+ const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
+
+ const {formatMessage} = this.props.intl;
+ let currentAuditDesc = '';
+
+ if (currentActionURL.indexOf('/channels') === 0) {
+ const channelInfo = currentAudit.extra_info.split(' ');
+ const channelNameField = channelInfo[0].split('=');
+
+ let channelURL = '';
+ let channelObj;
+ let channelName = '';
+ if (channelNameField.indexOf('name') >= 0) {
+ channelURL = channelNameField[channelNameField.indexOf('name') + 1];
+ channelObj = ChannelStore.getByName(channelURL);
+ if (channelObj) {
+ channelName = channelObj.display_name;
+ } else {
+ channelName = channelURL;
+ }
+ }
+
+ switch (currentActionURL) {
+ case '/channels/create':
+ currentAuditDesc = formatMessage(holders.channelCreated, {channelName: channelName});
+ break;
+ case '/channels/create_direct':
+ currentAuditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username});
+ break;
+ case '/channels/update':
+ currentAuditDesc = formatMessage(holders.nameUpdated, {channelName: channelName});
+ break;
+ case '/channels/update_desc': // support the old path
+ case '/channels/update_header':
+ currentAuditDesc = formatMessage(holders.headerUpdated, {channelName: channelName});
+ break;
+ default: {
+ let userIdField = [];
+ let userId = '';
+ let username = '';
+
+ if (channelInfo[1]) {
+ userIdField = channelInfo[1].split('=');
+
+ if (userIdField.indexOf('user_id') >= 0) {
+ userId = userIdField[userIdField.indexOf('user_id') + 1];
+ username = UserStore.getProfile(userId).username;
+ }
+ }
+
+ if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
+ currentAuditDesc = formatMessage(holders.channelDeleted, {url: channelURL});
+ } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
+ currentAuditDesc = formatMessage(holders.userAdded, {username: username, channelName: channelName});
+ } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
+ currentAuditDesc = formatMessage(holders.userRemoved, {username: username, channelName: channelName});
+ }
+
+ break;
+ }
+ }
+ } else if (currentActionURL.indexOf('/oauth') === 0) {
+ const oauthInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/oauth/register': {
+ const clientIdField = oauthInfo[0].split('=');
+
+ if (clientIdField[0] === 'client_id') {
+ currentAuditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
+ }
+
+ break;
+ }
+ case '/oauth/allow':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullOAuthAccess);
+ } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
+ currentAuditDesc = formatMessage(holders.failedOAuthAccess);
+ }
+
+ break;
+ case '/oauth/access_token':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedOAuthToken);
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullOAuthToken);
+ } else {
+ const oauthTokenFailure = oauthInfo[0].split('-');
+
+ if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
+ currentAuditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
+ }
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else if (currentActionURL.indexOf('/users') === 0) {
+ const userInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/users/login':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedLogin);
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullLogin);
+ } else if (userInfo[0]) {
+ currentAuditDesc = formatMessage(holders.failedLogin);
+ }
+
+ break;
+ case '/users/revoke_session':
+ currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
+ break;
+ case '/users/newimage':
+ currentAuditDesc = formatMessage(holders.updatePicture);
+ break;
+ case '/users/update':
+ currentAuditDesc = formatMessage(holders.updateGeneral);
+ break;
+ case '/users/newpassword':
+ if (userInfo[0] === 'attempted') {
+ currentAuditDesc = formatMessage(holders.attemptedPassword);
+ } else if (userInfo[0] === 'completed') {
+ currentAuditDesc = formatMessage(holders.successfullPassword);
+ } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
+ currentAuditDesc = formatMessage(holders.failedPassword);
+ }
+
+ break;
+ case '/users/update_roles': {
+ const userRoles = userInfo[0].split('=')[1];
+
+ currentAuditDesc = formatMessage(holders.updatedRol);
+ if (userRoles.trim()) {
+ currentAuditDesc += userRoles;
+ } else {
+ currentAuditDesc += formatMessage(holders.member);
+ }
+
+ break;
+ }
+ case '/users/update_active': {
+ const updateType = userInfo[0].split('=')[0];
+ const updateField = userInfo[0].split('=')[1];
+
+ /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
+ if (updateType === 'active') {
+ if (updateField === 'true') {
+ currentAuditDesc = formatMessage(holders.accountActive);
+ } else if (updateField === 'false') {
+ currentAuditDesc = formatMessage(holders.accountInactive);
+ }
+
+ const actingUserInfo = userInfo[1].split('=');
+ if (actingUserInfo[0] === 'session_user') {
+ const actingUser = UserStore.getProfile(actingUserInfo[1]);
+ const currentUser = UserStore.getCurrentUser();
+ if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
+ currentAuditDesc += formatMessage(holders.by, {username: actingUser.username});
+ } else if (currentUser && actingUser) {
+ currentAuditDesc += formatMessage(holders.byAdmin);
+ }
+ }
+ } else if (updateType === 'session_id') {
+ currentAuditDesc = this.handleRevokedSession(updateField);
+ }
+
+ break;
+ }
+ case '/users/send_password_reset':
+ currentAuditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
+ break;
+ case '/users/reset_password':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedReset);
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullReset);
+ }
+
+ break;
+ case '/users/update_notify':
+ currentAuditDesc = formatMessage(holders.updateGlobalNotifications);
+ break;
+ default:
+ break;
+ }
+ } else if (currentActionURL.indexOf('/hooks') === 0) {
+ const webhookInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/hooks/incoming/create':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedWebhookCreate);
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.succcessfullWebhookCreate);
+ } else if (webhookInfo[0] === 'fail - bad channel permissions') {
+ currentAuditDesc = formatMessage(holders.failedWebhookCreate);
+ }
+
+ break;
+ case '/hooks/incoming/delete':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = formatMessage(holders.attemptedWebhookDelete);
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = formatMessage(holders.successfullWebhookDelete);
+ } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
+ currentAuditDesc = formatMessage(holders.failedWebhookDelete);
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (currentActionURL) {
+ case '/logout':
+ currentAuditDesc = formatMessage(holders.logout);
+ break;
+ case '/verify_email':
+ currentAuditDesc = formatMessage(holders.verified);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* If all else fails... */
+ if (!currentAuditDesc) {
+ /* Currently not called anywhere */
+ if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
+ currentAuditDesc = formatMessage(holders.revokedAll);
+ } else {
+ let currentActionDesc = '';
+ if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
+ currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
+ currentActionDesc = Utils.toTitleCase(currentActionDesc);
+ }
+
+ let currentExtraInfoDesc = '';
+ if (currentAudit.extra_info) {
+ currentExtraInfoDesc = currentAudit.extra_info;
+
+ if (currentExtraInfoDesc.indexOf('=') !== -1) {
+ currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
+ }
+ }
+ currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
+ }
+ }
+
+ const currentDate = new Date(currentAudit.create_at);
+ let currentAuditInfo = currentDate.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + currentDate.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
+
+ if (this.props.showUserId) {
+ currentAuditInfo += ' | ' + formatMessage(holders.userId) + ': ' + currentAudit.user_id;
+ }
+
+ currentAuditInfo += ' | ' + currentAuditDesc;
+
+ return currentAuditInfo;
+ }
+ render() {
+ var accessList = [];
+
+ const {formatMessage} = this.props.intl;
+ for (var i = 0; i < this.props.audits.length; i++) {
+ const currentAudit = this.props.audits[i];
+ const currentAuditInfo = this.formatAuditInfo(currentAudit);
+
+ let moreInfo;
+ if (!this.props.oneLine) {
+ moreInfo = (
+ <a
+ href='#'
+ className='theme'
+ onClick={this.handleMoreInfo.bind(this, i)}
+ >
+ <FormattedMessage
+ id='audit_table.moreInfo'
+ defaultMessage='More info'
+ />
+ </a>
+ );
+ }
+
+ if (this.state.moreInfo[i]) {
+ if (!currentAudit.session_id) {
+ currentAudit.session_id = 'N/A';
+
+ if (currentAudit.action.search('/users/login') >= 0) {
+ if (currentAudit.extra_info === 'attempt') {
+ currentAudit.session_id += formatMessage(holders.loginAttempt);
+ } else {
+ currentAudit.session_id += formatMessage(holders.loginFailure);
+ }
+ }
+ }
+
+ moreInfo = (
+ <div>
+ <div>
+ <FormattedMessage
+ id='audit_table.ip'
+ defaultMessage='IP: {ip}'
+ values={{
+ ip: currentAudit.ip_address
+ }}
+ />
+ </div>
+ <div>
+ <FormattedMessage
+ id='audit_table.session'
+ defaultMessage='Session ID: {id}'
+ values={{
+ id: currentAudit.session_id
+ }}
+ />
+ </div>
+ </div>
+ );
+ }
+
+ var divider = null;
+ if (i < this.props.audits.length - 1) {
+ divider = (<div className='divider-light'></div>);
+ }
+
+ accessList[i] = (
+ <div
+ key={'accessHistoryEntryKey' + i}
+ className='access-history__table'
+ >
+ <div className='access__report'>
+ <div className='report__time'>{currentAuditInfo}</div>
+ <div className='report__info'>
+ {moreInfo}
+ </div>
+ {divider}
+ </div>
+ </div>
+ );
+ }
+
+ return <form role='form'>{accessList}</form>;
+ }
+}
+
+AuditTable.propTypes = {
+ intl: intlShape.isRequired,
+ audits: React.PropTypes.array.isRequired,
+ oneLine: React.PropTypes.bool,
+ showUserId: React.PropTypes.bool
+};
+
+export default injectIntl(AuditTable);
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
index 704e2ced4..8f43091a7 100644
--- a/web/react/stores/admin_store.jsx
+++ b/web/react/stores/admin_store.jsx
@@ -1,4 +1,4 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
@@ -10,6 +10,7 @@ import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
const LOG_CHANGE_EVENT = 'log_change';
+const SERVER_AUDIT_CHANGE_EVENT = 'server_audit_change';
const CONFIG_CHANGE_EVENT = 'config_change';
const ALL_TEAMS_EVENT = 'all_team_change';
@@ -18,6 +19,7 @@ class AdminStoreClass extends EventEmitter {
super();
this.logs = null;
+ this.audits = null;
this.config = null;
this.teams = null;
@@ -25,6 +27,10 @@ class AdminStoreClass extends EventEmitter {
this.addLogChangeListener = this.addLogChangeListener.bind(this);
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
+ this.emitAuditChange = this.emitAuditChange.bind(this);
+ this.addAuditChangeListener = this.addAuditChangeListener.bind(this);
+ this.removeAuditChangeListener = this.removeAuditChangeListener.bind(this);
+
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
@@ -46,6 +52,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(LOG_CHANGE_EVENT, callback);
}
+ emitAuditChange() {
+ this.emit(SERVER_AUDIT_CHANGE_EVENT);
+ }
+
+ addAuditChangeListener(callback) {
+ this.on(SERVER_AUDIT_CHANGE_EVENT, callback);
+ }
+
+ removeAuditChangeListener(callback) {
+ this.removeListener(SERVER_AUDIT_CHANGE_EVENT, callback);
+ }
+
emitConfigChange() {
this.emit(CONFIG_CHANGE_EVENT);
}
@@ -78,6 +96,14 @@ class AdminStoreClass extends EventEmitter {
this.logs = logs;
}
+ getAudits() {
+ return this.audits;
+ }
+
+ saveAudits(audits) {
+ this.audits = audits;
+ }
+
getConfig() {
return this.config;
}
@@ -113,6 +139,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveLogs(action.logs);
AdminStore.emitLogChange();
break;
+ case ActionTypes.RECIEVED_SERVER_AUDITS:
+ AdminStore.saveAudits(action.audits);
+ AdminStore.emitAuditChange();
+ break;
case ActionTypes.RECIEVED_CONFIG:
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 0ee89b9fa..d615e02c7 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -312,6 +312,32 @@ export function getLogs() {
);
}
+export function getServerAudits() {
+ if (isCallInProgress('getServerAudits')) {
+ return;
+ }
+
+ callTracker.getServerAudits = utils.getTimestamp();
+ client.getServerAudits(
+ (data, textStatus, xhr) => {
+ callTracker.getServerAudits = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_SERVER_AUDITS,
+ audits: data
+ });
+ },
+ (err) => {
+ callTracker.getServerAudits = 0;
+ dispatchError(err, 'getServerAudits');
+ }
+ );
+}
+
export function getConfig() {
if (isCallInProgress('getConfig')) {
return;
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 09cd4162a..473087308 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -385,6 +385,20 @@ export function getLogs(success, error) {
});
}
+export function getServerAudits(success, error) {
+ $.ajax({
+ url: '/api/v1/admin/audits',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getServerAudits', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getConfig(success, error) {
$.ajax({
url: '/api/v1/admin/config',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index e1a4b8a8a..fec9b27d7 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -45,6 +45,7 @@ export default {
RECIEVED_CONFIG: null,
RECIEVED_LOGS: null,
+ RECIEVED_SERVER_AUDITS: null,
RECIEVED_ALL_TEAMS: null,
SHOW_SEARCH: null,
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index d6401ab6e..d4c319145 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -8,53 +8,54 @@
"about.date": "Build Date:",
"about.hash": "Build Hash:",
"about.close": "Close",
- "access_history.sessionRevoked": "The session with id {sessionId} was revoked",
- "access_history.channelCreated": "Created the {channelName} channel/group",
- "access_history.establishedDM": "Established a direct message channel with {username}",
- "access_history.nameUpdated": "Updated the {channelName} channel/group name",
- "access_history.headerUpdated": "Updated the {channelName} channel/group header",
- "access_history.channelDeleted": "Deleted the channel/group with the URL {url}",
- "access_history.userAdded": "Added {username} to the {channelName} channel/group",
- "access_history.userRemoved": "Removed {username} to the {channelName} channel/group",
- "access_history.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}",
- "access_history.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access",
- "access_history.successfullOAuthAccess": "Successfully gave a new OAuth service access",
- "access_history.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback",
- "access_history.attemptedOAuthToken": "Attempted to get an OAuth access token",
- "access_history.successfullOAuthToken": "Successfully added a new OAuth service",
- "access_history.oauthTokenFailed": "Failed to get an OAuth access token - {token}",
- "access_history.attemptedLogin": "Attempted to login",
- "access_history.successfullLogin": "Successfully logged in",
- "access_history.failedLogin": "FAILED login attempt",
- "access_history.updatePicture": "Updated your profile picture",
- "access_history.updateGeneral": "Updated the general settings of your account",
- "access_history.attemptedPassword": "Attempted to change password",
- "access_history.successfullPassword": "Successfully changed password",
- "access_history.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth",
- "access_history.updatedRol": "Updated user role(s) to ",
- "access_history.member": "member",
- "access_history.accountActive": "Account made active",
- "access_history.accountInactive": "Account made inactive",
- "access_history.by": " by {username}",
- "access_history.byAdmin": " by an admin",
- "access_history.sentEmail": "Sent an email to {email} to reset your password",
- "access_history.attemptedReset": "Attempted to reset password",
- "access_history.successfullReset": "Successfully reset password",
- "access_history.updateGlobalNotifications": "Updated your global notification settings",
- "access_history.attemptedWebhookCreate": "Attempted to create a webhook",
- "access_history.successfullWebhookCreate": "Successfully created a webhook",
- "access_history.failedWebhookCreate": "Failed to create a webhook - bad channel permissions",
- "access_history.attemptedWebhookDelete": "Attempted to delete a webhook",
- "access_history.successfullWebhookDelete": "Successfully deleted a webhook",
- "access_history.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions",
- "access_history.logout": "Logged out of your account",
- "access_history.verified": "Sucessfully verified your email address",
- "access_history.revokedAll": "Revoked all current sessions for the team",
- "access_history.loginAttempt": " (Login attempt)",
- "access_history.loginFailure": " (Login failure)",
- "access_history.moreInfo": "More info",
- "access_history.ip": "IP: {ip}",
- "access_history.session": "Session ID: {id}",
+ "audit_table.sessionRevoked": "The session with id {sessionId} was revoked",
+ "audit_table.channelCreated": "Created the {channelName} channel/group",
+ "audit_table.establishedDM": "Established a direct message channel with {username}",
+ "audit_table.nameUpdated": "Updated the {channelName} channel/group name",
+ "audit_table.headerUpdated": "Updated the {channelName} channel/group header",
+ "audit_table.channelDeleted": "Deleted the channel/group with the URL {url}",
+ "audit_table.userAdded": "Added {username} to the {channelName} channel/group",
+ "audit_table.userRemoved": "Removed {username} to the {channelName} channel/group",
+ "audit_table.attemptedRegisterApp": "Attempted to register a new OAuth Application with ID {id}",
+ "audit_table.attemptedAllowOAuthAccess": "Attempted to allow a new OAuth service access",
+ "audit_table.successfullOAuthAccess": "Successfully gave a new OAuth service access",
+ "audit_table.failedOAuthAccess": "Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback",
+ "audit_table.attemptedOAuthToken": "Attempted to get an OAuth access token",
+ "audit_table.successfullOAuthToken": "Successfully added a new OAuth service",
+ "audit_table.oauthTokenFailed": "Failed to get an OAuth access token - {token}",
+ "audit_table.attemptedLogin": "Attempted to login",
+ "audit_table.successfullLogin": "Successfully logged in",
+ "audit_table.failedLogin": "FAILED login attempt",
+ "audit_table.updatePicture": "Updated your profile picture",
+ "audit_table.updateGeneral": "Updated the general settings of your account",
+ "audit_table.attemptedPassword": "Attempted to change password",
+ "audit_table.successfullPassword": "Successfully changed password",
+ "audit_table.failedPassword": "Failed to change password - tried to update user password who was logged in through oauth",
+ "audit_table.updatedRol": "Updated user role(s) to ",
+ "audit_table.member": "member",
+ "audit_table.accountActive": "Account made active",
+ "audit_table.accountInactive": "Account made inactive",
+ "audit_table.by": " by {username}",
+ "audit_table.byAdmin": " by an admin",
+ "audit_table.sentEmail": "Sent an email to {email} to reset your password",
+ "audit_table.attemptedReset": "Attempted to reset password",
+ "audit_table.successfullReset": "Successfully reset password",
+ "audit_table.updateGlobalNotifications": "Updated your global notification settings",
+ "audit_table.attemptedWebhookCreate": "Attempted to create a webhook",
+ "audit_table.successfullWebhookCreate": "Successfully created a webhook",
+ "audit_table.failedWebhookCreate": "Failed to create a webhook - bad channel permissions",
+ "audit_table.attemptedWebhookDelete": "Attempted to delete a webhook",
+ "audit_table.successfullWebhookDelete": "Successfully deleted a webhook",
+ "audit_table.failedWebhookDelete": "Failed to delete a webhook - inappropriate conditions",
+ "audit_table.logout": "Logged out of your account",
+ "audit_table.verified": "Sucessfully verified your email address",
+ "audit_table.revokedAll": "Revoked all current sessions for the team",
+ "audit_table.loginAttempt": " (Login attempt)",
+ "audit_table.loginFailure": " (Login failure)",
+ "audit_table.moreInfo": "More info",
+ "audit_table.ip": "IP: {ip}",
+ "audit_table.session": "Session ID: {id}",
+ "audit_table.userId": "User ID",
"access_history.title": "Access History",
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
"activity_log_modal.androidNativeApp": "Android Native App",
@@ -96,6 +97,7 @@
"admin.sidebar.teams": "TEAMS ({count})",
"admin.sidebar.other": "OTHER",
"admin.sidebar.logs": "Logs",
+ "admin.sidebar.audits": "Audits",
"admin.analytics.loading": "Loading...",
"admin.analytics.totalUsers": "Total Users",
"admin.analytics.publicChannels": "Public Channels",
@@ -326,6 +328,8 @@
"admin.log.save": "Save",
"admin.logs.title": "Server Logs",
"admin.logs.reload": "Reload",
+ "admin.audits.title": "Server Audits",
+ "admin.audits.reload": "Reload",
"admin.privacy.saving": "Saving Config...",
"admin.privacy.title": "Privacy Settings",
"admin.privacy.showEmailTitle": "Show Email Address: ",
@@ -1044,4 +1048,4 @@
"user.settings.security.title": "Security Settings",
"user.settings.security.viewHistory": "View Access History",
"user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions"
-} \ No newline at end of file
+}