From d153d661db7d4349d69824d318aa9ad571970606 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Tue, 2 Feb 2016 16:49:27 -0500 Subject: Add basic server audit tab to system console for EE --- web/react/components/access_history_modal.jsx | 548 +------------------- .../components/admin_console/admin_controller.jsx | 3 + .../components/admin_console/admin_sidebar.jsx | 19 + web/react/components/admin_console/audits.jsx | 94 ++++ web/react/components/audit_table.jsx | 571 +++++++++++++++++++++ web/react/stores/admin_store.jsx | 32 +- web/react/utils/async_client.jsx | 26 + web/react/utils/client.jsx | 14 + web/react/utils/constants.jsx | 1 + web/static/i18n/en.json | 100 ++-- 10 files changed, 824 insertions(+), 584 deletions(-) create mode 100644 web/react/components/admin_console/audits.jsx create mode 100644 web/react/components/audit_table.jsx (limited to 'web') 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 = ( - - - - ); - - 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 = ( -
-
- -
-
- -
-
- ); - } - - var divider = null; - if (i < this.state.audits.length - 1) { - divider = (
); - } - - accessList[i] = ( -
-
-
{currentAuditInfo}
-
- {moreInfo} -
- {divider} -
-
- ); - } - var content; if (this.state.audits.loading) { content = (); } else { - content = (
{accessList}
); + content = ( + + ); } 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 = ; } else if (this.state.selected === 'logs') { tab = ; + } else if (this.state.selected === 'audits') { + tab = ; } else if (this.state.selected === 'image_settings') { tab = ; } 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 = ( +
  • + + + +
  • + ); + } + return (
    @@ -448,6 +466,7 @@ export default class AdminSidebar extends React.Component { /> + {audits} 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
    ; + } + + if (this.state.audits === null) { + content = ; + } else { + content = ( +
    + +
    + ); + } + + return ( +
    +

    + +

    + +
    + {content} +
    +
    + ); + } +} 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 = ( + + + + ); + } + + 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 = ( +
    +
    + +
    +
    + +
    +
    + ); + } + + var divider = null; + if (i < this.props.audits.length - 1) { + divider = (
    ); + } + + accessList[i] = ( +
    +
    +
    {currentAuditInfo}
    +
    + {moreInfo} +
    + {divider} +
    +
    + ); + } + + return
    {accessList}
    ; + } +} + +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 +} -- cgit v1.2.3-1-g7c22