summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/README.md4
-rw-r--r--doc/install/Production-Ubuntu.md2
-rw-r--r--doc/install/SMTP-Email-Setup.md81
-rw-r--r--web/react/components/access_history_modal.jsx299
4 files changed, 319 insertions, 67 deletions
diff --git a/doc/README.md b/doc/README.md
index 7ee2bb46c..ccb702a5d 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -8,15 +8,17 @@ Get up and running quickly with Docker-based install
- [AWS Elastic Beanstalk Setup](install/Amazon-Elastic-Beanstalk.md)
- [Docker Single Container Preview Setup](install/Docker-Single-Container.md)
- [SMTP Email Setup](install/SMTP-Email-Setup.md)
+- [System Console Settings](install/Configuration-Settings.md)
#### Production Installation
Set up Mattermost in your data center
- [Software and Hardware Requirements](install/Requirements.md)
- [Production Ubuntu Setup](install/Production-Ubuntu.md)
- [SMTP Email Setup](install/SMTP-Email-Setup.md)
+- [System Console Settings](install/Configuration-Settings.md)
#### Configuration and Management
-- Configuration Settings Overview
+- [System Console Settings](install/Configuration-Settings.md)
- [GitLab SSO Configuration](integrations/Single-Sign-On/Gitlab.md)
- [Mattermost Release Numbering Scheme](install/Release-Numbering.md)
diff --git a/doc/install/Production-Ubuntu.md b/doc/install/Production-Ubuntu.md
index 05a56c412..87f8edb84 100644
--- a/doc/install/Production-Ubuntu.md
+++ b/doc/install/Production-Ubuntu.md
@@ -28,7 +28,7 @@
## Set up Mattermost Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.2
1. Download the latest Mattermost Server by typing:
- * ``` wget https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz```
+ * ``` wget https://github.com/mattermost/platform/releases/download/v1.1.0/mattermost.tar.gz```
1. Unzip the Mattermost Server by typing:
* ``` tar -xvzf mattermost.tar.gz```
1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`.
diff --git a/doc/install/SMTP-Email-Setup.md b/doc/install/SMTP-Email-Setup.md
index 8bf094714..4e06d2f99 100644
--- a/doc/install/SMTP-Email-Setup.md
+++ b/doc/install/SMTP-Email-Setup.md
@@ -1,55 +1,46 @@
## SMTP Email Setup
-In some product evaluation setups, email is intentionally bypassed by setting `SendEmailNotifications=false`. This option allows account creation and system operation without having to set up an email service (e.g. no email verification is required for account creation). This also means neither email notifications nor password reset by email are available.
-
-To enable email, turn this option on by setting `SendEmailNotifications=true` and configuring an SMTP email service as follows:
-
-1. **Set up an SMTP email sending service.** (If you already have credentials for a SMTP server you can skip this step.)
- 1. [Setup Amazon Simple Email Service](https://console.aws.amazon.com/ses)
- 2. From the `SMTP Settings` menu click `Create My SMTP Credentials`
- 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password`
- 4. From the `Domains` menu setup and verify a new domain. It it also a good practice to enable `Generate DKIM Settings` for this domain.
- 5. Choose an email address like `mattermost@example.com` for Mattermost to send emails from.
- 6. Test sending an email from `mattermost@example.com` by clicking the `Send a Test Email` button and verify everything appears to be working correctly.
-2. **Modify the Mattermost configuration file config.json or config_docker.json with the SMTP information.**
- 1. If you're running Mattermost on Amazon Beanstalk you can shell into the instance with the following commands
- 2. `ssh ec2-user@[domain for the docker instance]`
- 3. `sudo gpasswd -a ec2-user docker`
- 4. Retrieve the name of the container with `sudo docker ps`
- 5. `sudo docker exec -ti container_name /bin/bash`
-3. **Edit the config file `vi /config_docker.json` with the settings you captured from the step above.**
- 1. See an example below and notice `SendEmailNotifications` has been set to `true`
- ```
- "EmailSettings": {
- "EnableSignUpWithEmail": true,
- "SendEmailNotifications": true,
- "RequireEmailVerification": true,
- "FeedbackName": "No-Reply",
- "FeedbackEmail": "mattermost@example.com",
- "SMTPUsername": "AFIADTOVDKDLGERR",
- "SMTPPassword": "DFKJoiweklsjdflkjOIGHLSDFJewiskdjf",
- "SMTPServer": "email-smtp.us-east-1.amazonaws.com",
- "SMTPPort": "465",
- "ConnectionSecurity": "TLS",
- "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS",
- "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
- },
- ```
-4. **Restart Mattermost**
- 1. Find the process id with `ps -A` and look for the process named `platform`
- 2. Kill the process `kill pid`
- 3. The service should restart automatically. Verify the Mattermost service is running with `ps -A`
- 4. Current logged in users will not be affected, but upon logging out or session expiration users will be required to verify their email address.
+In product evaluation setups with single-container Docker instances, email is intentionally disabled. This allows account creation and system operation without having to set up email, but it also means email notification and password reset functionality aren't available.
+
+### How to enable email
+
+To enable email, configure an SMTP email service as follows:
+
+1. **Set up an SMTP email sending service** (if you don't yet have an SMTP service with credentials)
+ 1. Any SMTP email service can be used, you just need the following information: `Server Name`, `Port`, `SMTP Username`, and `SMTP Password`.
+ 2. If you don't have an SMTP service, here are simple instructions to set one up with [Amazon Simple Email Service (SES)](https://aws.amazon.com/ses/):
+ 2. Go to [Amazon SES console](https://console.aws.amazon.com/ses) then `SMTP Settings > Create My SMTP Credentials`
+ 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password` for Step 2 below.
+ 4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain.
+ 5. Choose an sender address like `mattermost@example.com` and click `Send a Test Email` to verify setup is working correctly.
+2. **Configure SMTP settings**
+ 1. Open the **System Console** by logging into an existing team and accessing "System Console" from the main menu.
+ 1. Alternatively, if a team doesn't yet exist, go to `http://dockerhost:8065/` in your browser, create a team, then from the main menu click **System Console**
+ 2. Go to the **Email Settings** tab and configure the following:
+ 1. **Allow Sign Up With Email:** `true`
+ 2. **Send Email Notifications:** `true`
+ 3. **Require Email Verification:** `true`
+ 4. **Notification Display Name:** Display name on email account sending notifications
+ 5. **Notification Email Address:** Email address displayed on email account used to send notifications
+ 6. **SMTP Username**: `SMTP Username` from Step 1
+ 7. **SMTP Password**: `SMTP Password` from Step 1
+ 8. **SMTP Server**: `SMTP Server` from Step 1
+ 9. **SMTP Port**: `SMTP Port` from Step 1
+ 10. **Connection Security**: `TLS (Recommended)`
+ 11. Then click **Save**
+
+3. **Restart Mattermost**
+ 1. Use `ps -A` to find the process ID ("pid") for service named `platform` and stop it using `kill [pid]`
+ 2. The service should restart automatically. Run `ps -A` to verify the `platform` is running again
+ 3. Use the reset password page (E.g. _example.com/teamname/reset_password_) to test that email is now working by entering your email and clicking **Reset my password**.
+ 4. Note: The next time users log out, or when their session tokens expire, each will be required to verify their email address.
### Troubleshooting SMTP
-If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open `System Console > Logs` to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments.
+If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open **System Console -> Logs** to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments.
-For example, if `System Console > Logs` has an error code reading:
+For example, if **System Console -> Logs** has an error code reading:
```
Connection unsuccessful: Failed to add to email address - 554 5.7.1 <unknown[IP-ADDRESS]>: Client host rejected: Access denied
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 2ad4d5b00..909639859 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+var ChannelStore = require('../stores/channel_store.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var LoadingScreen = require('./loading_screen.jsx');
var Utils = require('../utils/utils.jsx');
@@ -14,8 +15,10 @@ export default class AccessHistoryModal extends React.Component {
this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.onHide = this.onHide.bind(this);
this.onShow = this.onShow.bind(this);
+ this.formatAuditInfo = this.formatAuditInfo.bind(this);
+ this.handleRevokedSession = this.handleRevokedSession.bind(this);
- let state = this.getStateFromStoresForAudits();
+ const state = this.getStateFromStoresForAudits();
state.moreInfo = [];
this.state = state;
@@ -52,23 +55,269 @@ export default class AccessHistoryModal extends React.Component {
newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo});
}
- render() {
- var accessList = [];
- var currentHistoryDate = null;
+ handleRevokedSession(sessionId) {
+ return 'The session with id ' + sessionId + ' was revoked';
+ }
+ formatAuditInfo(currentAudit) {
+ const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
- for (var i = 0; i < this.state.audits.length; i++) {
- var currentAudit = this.state.audits[i];
- var newHistoryDate = new Date(currentAudit.create_at);
- var newDate = null;
+ 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 = 'Created the ' + channelName + ' channel/group';
+ break;
+ case '/channels/create_direct':
+ currentAuditDesc = 'Established a direct message channel with ' + Utils.getDirectTeammate(channelObj.id).username;
+ break;
+ case '/channels/update':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group name';
+ break;
+ case '/channels/update_desc':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group description';
+ 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 = 'Deleted the channel/group with the URL ' + channelURL;
+ } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
+ currentAuditDesc = 'Added ' + username + ' to the ' + channelName + ' channel/group';
+ } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
+ currentAuditDesc = 'Removed ' + username + ' from the ' + channelName + ' channel/group';
+ }
+
+ 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 = 'Attempted to register a new OAuth Application with ID ' + clientIdField[1];
+ }
+
+ break;
+ case '/oauth/allow':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to allow a new OAuth service access';
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully gave a new OAuth service access';
+ } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
+ currentAuditDesc = 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback';
+ }
+
+ break;
+ case '/oauth/access_token':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to get an OAuth access token';
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully added a new OAuth service';
+ } else {
+ const oauthTokenFailure = oauthInfo[0].split('-');
+
+ if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
+ currentAuditDesc = 'Failed to get an OAuth access 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 = 'Attempted to login';
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully logged in';
+ } else if (userInfo[0]) {
+ currentAuditDesc = 'FAILED login attempt';
+ }
+
+ break;
+ case '/users/revoke_session':
+ currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
+ break;
+ case '/users/newimage':
+ currentAuditDesc = 'Updated your profile picture';
+ break;
+ case '/users/update':
+ currentAuditDesc = 'Updated the general settings of your account';
+ break;
+ case '/users/newpassword':
+ if (userInfo[0] === 'attempted') {
+ currentAuditDesc = 'Attempted to change password';
+ } else if (userInfo[0] === 'completed') {
+ currentAuditDesc = 'Successfully changed password';
+ } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
+ currentAuditDesc = 'Failed to change password - tried to update user password who was logged in through oauth';
+ }
+
+ break;
+ case '/users/update_roles':
+ const userRoles = userInfo[0].split('=')[1];
- if (!currentHistoryDate || currentHistoryDate.toLocaleDateString() !== newHistoryDate.toLocaleDateString()) {
- currentHistoryDate = newHistoryDate;
- newDate = (<div> {currentHistoryDate.toDateString()} </div>);
+ currentAuditDesc = 'Updated user role(s) to ';
+ if (userRoles.trim()) {
+ currentAuditDesc += userRoles;
+ } else {
+ currentAuditDesc += '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 = 'Account made active';
+ } else if (updateField === 'false') {
+ currentAuditDesc = 'Account made inactive';
+ }
+
+ 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 += ' by ' + actingUser.username;
+ } else if (currentUser && actingUser) {
+ currentAuditDesc += ' by an admin';
+ }
+ }
+ } else if (updateType === 'session_id') {
+ currentAuditDesc = this.handleRevokedSession(updateField);
+ }
+
+ break;
+ case '/users/send_password_reset':
+ currentAuditDesc = 'Sent an email to ' + userInfo[0].split('=')[1] + ' to reset your password';
+ break;
+ case '/users/reset_password':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to reset password';
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully reset password';
+ }
+
+ break;
+ case '/users/update_notify':
+ currentAuditDesc = 'Updated your global notification settings';
+ 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 = 'Attempted to create a webhook';
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully created a webhook';
+ } else if (webhookInfo[0] === 'fail - bad channel permissions') {
+ currentAuditDesc = 'Failed to create a webhook - bad channel permissions';
+ }
- if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) {
- currentAudit.session_id = 'N/A (Login attempt)';
+ break;
+ case '/hooks/incoming/delete':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to delete a webhook';
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully deleted a webhook';
+ } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
+ currentAuditDesc = 'Failed to delete a webhook - inappropriate conditions';
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (currentActionURL) {
+ case '/logout':
+ currentAuditDesc = 'Logged out of your account';
+ break;
+ case '/verify_email':
+ currentAuditDesc = 'Sucessfully verified your email address';
+ break;
+ default:
+ break;
}
+ }
+
+ /* If all else fails... */
+ if (!currentAuditDesc) {
+ /* Currently not called anywhere */
+ if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
+ currentAuditDesc = 'Revoked all current sessions for the team';
+ } 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.toDateString() + ' - ' + currentDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc;
+ return currentAuditInfo;
+ }
+ render() {
+ var accessList = [];
+
+ for (var i = 0; i < this.state.audits.length; i++) {
+ const currentAudit = this.state.audits[i];
+ const currentAuditInfo = this.formatAuditInfo(currentAudit);
var moreInfo = (
<a
@@ -76,15 +325,27 @@ export default class AccessHistoryModal extends React.Component {
className='theme'
onClick={this.handleMoreInfo.bind(this, i)}
>
- More info
+ {'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 += ' (Login attempt)';
+ } else {
+ currentAudit.session_id += ' (Login failure)';
+ }
+ }
+ }
+
moreInfo = (
<div>
+ <div>{'IP: ' + currentAudit.ip_address}</div>
<div>{'Session ID: ' + currentAudit.session_id}</div>
- <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div>
</div>
);
}
@@ -99,11 +360,9 @@ export default class AccessHistoryModal extends React.Component {
key={'accessHistoryEntryKey' + i}
className='access-history__table'
>
- <div className='access__date'>{newDate}</div>
<div className='access__report'>
- <div className='report__time'>{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}</div>
+ <div className='report__time'>{currentAuditInfo}</div>
<div className='report__info'>
- <div>{'IP: ' + currentAudit.ip_address}</div>
{moreInfo}
</div>
{divider}
@@ -138,13 +397,13 @@ export default class AccessHistoryModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
id='myModalLabel'
>
- Access History
+ {'Access History'}
</h4>
</div>
<div