diff options
Diffstat (limited to 'webapp/components/user_settings')
-rw-r--r-- | webapp/components/user_settings/user_settings_security/index.js | 16 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_security/user_settings_security.jsx | 470 |
2 files changed, 455 insertions, 31 deletions
diff --git a/webapp/components/user_settings/user_settings_security/index.js b/webapp/components/user_settings/user_settings_security/index.js index cdbabd055..a3e83d7de 100644 --- a/webapp/components/user_settings/user_settings_security/index.js +++ b/webapp/components/user_settings/user_settings_security/index.js @@ -3,20 +3,30 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {getMe} from 'mattermost-redux/actions/users'; +import {getMe, getUserAccessTokensForUser, createUserAccessToken, revokeUserAccessToken, clearUserAccessTokens} from 'mattermost-redux/actions/users'; +import * as UserUtils from 'mattermost-redux/utils/user_utils'; import SecurityTab from './user_settings_security.jsx'; function mapStateToProps(state, ownProps) { + const tokensEnabled = state.entities.general.config.EnableUserAccessTokens === 'true'; + const userHasTokenRole = UserUtils.hasUserAccessTokenRole(ownProps.user.roles) || UserUtils.isSystemAdmin(ownProps.user.roles); + return { - ...ownProps + ...ownProps, + userAccessTokens: state.entities.users.myUserAccessTokens, + canUseAccessTokens: tokensEnabled && userHasTokenRole }; } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - getMe + getMe, + getUserAccessTokensForUser, + createUserAccessToken, + revokeUserAccessToken, + clearUserAccessTokens }, dispatch) }; } diff --git a/webapp/components/user_settings/user_settings_security/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security/user_settings_security.jsx index b8ec690a4..5c9ad67e3 100644 --- a/webapp/components/user_settings/user_settings_security/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security/user_settings_security.jsx @@ -6,6 +6,7 @@ import SettingItemMax from 'components/setting_item_max.jsx'; import AccessHistoryModal from 'components/access_history_modal'; import ActivityLogModal from 'components/activity_log_modal'; import ToggleModalButton from 'components/toggle_modal_button.jsx'; +import ConfirmModal from 'components/confirm_modal.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; @@ -13,15 +14,22 @@ import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; import {updatePassword, getAuthorizedApps, deactivateMfa, deauthorizeOAuthApp} from 'actions/user_actions.jsx'; +import {trackEvent} from 'actions/diagnostics_actions.jsx'; +import {isMobile} from 'utils/user_agent.jsx'; import $ from 'jquery'; import PropTypes from 'prop-types'; import React from 'react'; -import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; +import * as UserUtils from 'mattermost-redux/utils/user_utils'; +import {FormattedMessage, FormattedTime, FormattedDate, FormattedHTMLMessage} from 'react-intl'; import {browserHistory, Link} from 'react-router/es6'; import icon50 from 'images/icon50x50.png'; +const TOKEN_CREATING = 'creating'; +const TOKEN_CREATED = 'created'; +const TOKEN_NOT_CREATING = 'not_creating'; + export default class SecurityTab extends React.Component { static propTypes = { user: PropTypes.object, @@ -31,26 +39,45 @@ export default class SecurityTab extends React.Component { closeModal: PropTypes.func.isRequired, collapseModal: PropTypes.func.isRequired, setEnforceFocus: PropTypes.func.isRequired, + + /* + * The user access tokens for the user + */ + userAccessTokens: PropTypes.object, + + /* + * Set if access tokens are enabled and this user can use them + */ + canUseAccessTokens: PropTypes.bool, + actions: PropTypes.shape({ - getMe: PropTypes.func.isRequired + getMe: PropTypes.func.isRequired, + + /* + * Function to get user access tokens for a user + */ + getUserAccessTokensForUser: PropTypes.func.isRequired, + + /* + * Function to create a user access token + */ + createUserAccessToken: PropTypes.func.isRequired, + + /* + * Function to revoke a user access token + */ + revokeUserAccessToken: PropTypes.func.isRequired, + + /* + * Function to clear user access tokens locally + */ + clearUserAccessTokens: PropTypes.func.isRequired }).isRequired } constructor(props) { super(props); - this.submitPassword = this.submitPassword.bind(this); - this.setupMfa = this.setupMfa.bind(this); - this.removeMfa = this.removeMfa.bind(this); - this.updateCurrentPassword = this.updateCurrentPassword.bind(this); - this.updateNewPassword = this.updateNewPassword.bind(this); - this.updateConfirmPassword = this.updateConfirmPassword.bind(this); - this.getDefaultState = this.getDefaultState.bind(this); - this.createPasswordSection = this.createPasswordSection.bind(this); - this.createSignInSection = this.createSignInSection.bind(this); - this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this); - this.deauthorizeApp = this.deauthorizeApp.bind(this); - this.state = this.getDefaultState(); } @@ -61,6 +88,8 @@ export default class SecurityTab extends React.Component { confirmPassword: '', passwordError: '', serverError: '', + tokenError: '', + showConfirmModal: false, authService: this.props.user.auth_service }; } @@ -73,11 +102,18 @@ export default class SecurityTab extends React.Component { }, (err) => { this.setState({serverError: err.message}); //eslint-disable-line react/no-did-mount-set-state - }); + } + ); + } + + if (this.props.canUseAccessTokens) { + this.props.actions.clearUserAccessTokens(); + const userId = this.props.user ? this.props.user.id : ''; + this.props.actions.getUserAccessTokensForUser(userId, 0, 200); } } - submitPassword(e) { + submitPassword = (e) => { e.preventDefault(); var user = this.props.user; @@ -127,12 +163,12 @@ export default class SecurityTab extends React.Component { ); } - setupMfa(e) { + setupMfa = (e) => { e.preventDefault(); browserHistory.push('/mfa/setup'); } - removeMfa() { + removeMfa = () => { deactivateMfa( () => { if (global.window.mm_license.MFA === 'true' && @@ -157,19 +193,19 @@ export default class SecurityTab extends React.Component { ); } - updateCurrentPassword(e) { + updateCurrentPassword = (e) => { this.setState({currentPassword: e.target.value}); } - updateNewPassword(e) { + updateNewPassword = (e) => { this.setState({newPassword: e.target.value}); } - updateConfirmPassword(e) { + updateConfirmPassword = (e) => { this.setState({confirmPassword: e.target.value}); } - deauthorizeApp(e) { + deauthorizeApp = (e) => { e.preventDefault(); const appId = e.currentTarget.getAttribute('data-app'); deauthorizeOAuthApp( @@ -183,10 +219,11 @@ export default class SecurityTab extends React.Component { }, (err) => { this.setState({serverError: err.message}); - }); + } + ); } - createMfaSection() { + createMfaSection = () => { let updateSectionStatus; let submit; @@ -321,7 +358,7 @@ export default class SecurityTab extends React.Component { ); } - createPasswordSection() { + createPasswordSection = () => { let updateSectionStatus; if (this.props.activeSection === 'password') { @@ -578,7 +615,7 @@ export default class SecurityTab extends React.Component { ); } - createSignInSection() { + createSignInSection = () => { let updateSectionStatus; const user = this.props.user; @@ -793,7 +830,7 @@ export default class SecurityTab extends React.Component { ); } - createOAuthAppsSection() { + createOAuthAppsSection = () => { let updateSectionStatus; if (this.props.activeSection === 'apps') { @@ -929,6 +966,368 @@ export default class SecurityTab extends React.Component { ); } + startCreatingToken = () => { + this.setState({tokenCreationState: TOKEN_CREATING}); + } + + stopCreatingToken = () => { + this.setState({tokenCreationState: TOKEN_NOT_CREATING}); + } + + handleCreateToken = async () => { + this.handleCancelConfirm(); + + const description = this.refs.newtokendescription ? this.refs.newtokendescription.value : ''; + + if (description === '') { + this.setState({tokenError: Utils.localizeMessage('user.settings.tokens.nameRequired', 'Please enter a name.')}); + return; + } + + this.setState({tokenError: ''}); + + const userId = this.props.user ? this.props.user.id : ''; + const {data, error} = await this.props.actions.createUserAccessToken(userId, description); + + if (data) { + this.setState({tokenCreationState: TOKEN_CREATED, newToken: data}); + } else if (error) { + this.setState({serverError: error.message}); + } + } + + handleCancelConfirm = () => { + this.setState({ + showConfirmModal: false, + confirmTitle: null, + confirmMessage: null, + confirmButton: null, + confirmComplete: null + }); + } + + confirmCreateToken = () => { + if (UserUtils.isSystemAdmin(this.props.user.roles)) { + this.setState({ + showConfirmModal: true, + confirmTitle: ( + <FormattedMessage + id='user.settings.tokens.confirmCreateTitle' + defaultMessage='Create System Admin User Access Token' + /> + ), + confirmMessage: ( + <div className='alert alert-danger'> + <FormattedHTMLMessage + id='user.settings.tokens.confirmCreateMessage' + defaultMessage='You are generating a user access token with System Admin permissions. Are you sure want to create this token?' + /> + </div> + ), + confirmButton: ( + <FormattedMessage + id='user.settings.tokens.confirmCreateButton' + defaultMessage='Yes, Create' + /> + ), + confirmComplete: () => { + this.handleCreateToken(); + trackEvent('settings', 'system_admin_create_user_access_token'); + } + }); + + return; + } + + this.handleCreateToken(); + } + + saveTokenKeyPress = (e) => { + if (e.which === Constants.KeyCodes.ENTER) { + this.confirmCreateToken(); + } + } + + confirmRevokeToken = (tokenId) => { + const token = this.props.userAccessTokens[tokenId]; + + this.setState({ + showConfirmModal: true, + confirmTitle: ( + <FormattedMessage + id='user.settings.tokens.confirmDeleteTitle' + defaultMessage='Delete {name} Token?' + values={{ + name: token.description + }} + /> + ), + confirmMessage: ( + <div className='alert alert-danger'> + <FormattedHTMLMessage + id='user.settings.tokens.confirmDeleteMessage' + defaultMessage='Any integrations using this token will no longer be able to access the Mattermost API. You cannot undo this action. Are you sure want to delete this token?' + /> + </div> + ), + confirmButton: ( + <FormattedMessage + id='user.settings.tokens.confirmDeleteButton' + defaultMessage='Yes, Delete' + /> + ), + confirmComplete: () => { + this.revokeToken(tokenId); + trackEvent('settings', 'revoke_user_access_token'); + } + }); + } + + revokeToken = async (tokenId) => { + const {error} = await this.props.actions.revokeUserAccessToken(tokenId); + if (error) { + this.setState({serverError: error.message}); + } + this.handleCancelConfirm(); + } + + createTokensSection = () => { + let updateSectionStatus; + + if (this.props.activeSection === 'tokens') { + const tokenList = []; + Object.values(this.props.userAccessTokens).forEach((token) => { + if (this.state.newToken && this.state.newToken.id === token.id) { + return; + } + + tokenList.push( + <div + key={token.id} + className='setting-box__item' + > + <div className='whitespace--nowrap overflow--ellipsis'> + <strong>{token.description}</strong> + </div> + <div className='setting-box__token-id whitespace--nowrap overflow--ellipsis'> + <FormattedMessage + id='user.settings.tokens.tokenId' + defaultMessage='Token ID: ' + /> + {token.id} + </div> + <div> + <a + name={token.id} + href='#' + onClick={(e) => { + e.preventDefault(); + this.confirmRevokeToken(token.id); + }} + > + <FormattedMessage + id='user.settings.tokens.delete' + defaultMessage='Delete' + /> + </a> + </div> + <hr className='margin-bottom margin-top x2'/> + </div> + ); + }); + + if (tokenList.length === 0) { + tokenList.push( + <FormattedMessage + key='notokens' + id='user.settings.tokens.userAccessTokensNone' + defaultMessage='No user access tokens.' + /> + ); + } + let extraInfo; + + if (isMobile()) { + extraInfo = ( + <span> + <FormattedHTMLMessage + id='user.settings.tokens.description_mobile' + defaultMessage='<a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">User access tokens</a> function similar to session tokens and can be used by integrations to <a href="https://about.mattermost.com/default-api-authentication" target="_blank">authenticate against the REST API</a>. Create new tokens on your desktop.' + /> + </span> + ); + } else { + extraInfo = ( + <span> + <FormattedHTMLMessage + id='user.settings.tokens.description' + defaultMessage='<a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">User access tokens</a> function similar to session tokens and can be used by integrations to <a href="https://about.mattermost.com/default-api-authentication" target="_blank">authenticate against the REST API</a>.' + /> + </span> + ); + } + + let newTokenSection; + if (this.state.tokenCreationState === TOKEN_CREATING) { + newTokenSection = ( + <div className='padding-left x2'> + <div className='row'> + <label className='col-sm-auto control-label padding-right x2'> + <FormattedMessage + id='user.settings.tokens.name' + defaultMessage='Name: ' + /> + </label> + <div className='col-sm-5'> + <input + ref='newtokendescription' + className='form-control' + type='text' + maxLength={64} + onKeyPress={this.saveTokenKeyPress} + /> + </div> + </div> + <div> + <div className='padding-top x2'> + <FormattedMessage + id='user.settings.tokens.nameDescription' + defaultMessage='Give a name for your token, so you remember what it’s used for. A token is generated after you hit "Save".' + /> + </div> + <div> + <label + id='clientError' + className='has-error margin-top margin-bottom' + > + {this.state.tokenError} + </label> + </div> + <button + className='btn btn-primary' + onClick={this.confirmCreateToken} + > + <FormattedMessage + id='user.settings.tokens.save' + defaultMessage='Save' + /> + </button> + <button + className='btn btn-default' + onClick={this.stopCreatingToken} + > + <FormattedMessage + id='user.settings.tokens.cancel' + defaultMessage='Cancel' + /> + </button> + </div> + </div> + ); + } else if (this.state.tokenCreationState === TOKEN_CREATED) { + newTokenSection = ( + <div + className='alert alert-warning' + > + <i className='fa fa-warning margin-right'/> + <FormattedMessage + id='user.settings.tokens.copy' + defaultMessage="Please copy the token below. You won't be able to see it again!" + /> + <br/> + <br/> + <FormattedMessage + id='user.settings.tokens.name' + defaultMessage='Name: ' + /> + {this.state.newToken.description} + <br/> + <FormattedMessage + id='user.settings.tokens.id' + defaultMessage='ID: ' + /> + {this.state.newToken.id} + <br/> + <strong> + <FormattedMessage + id='user.settings.tokens.token' + defaultMessage='Token: ' + /> + {this.state.newToken.token} + </strong> + </div> + ); + } else { + newTokenSection = ( + <a + className='btn btn-primary' + href='#' + onClick={this.startCreatingToken} + > + <FormattedMessage + id='user.settings.tokens.create' + defaultMessage='Create New Token' + /> + </a> + ); + } + + const inputs = []; + inputs.push( + <div + key='tokensSetting' + className='padding-top' + > + <div key='tokenList'> + <div className='alert alert-transparent'> + {tokenList} + </div> + <br/> + {newTokenSection} + </div> + </div> + ); + + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({newToken: null, tokenCreationState: TOKEN_NOT_CREATING, serverError: null, tokenError: ''}); + e.preventDefault(); + }.bind(this); + + return ( + <SettingItemMax + title={Utils.localizeMessage('user.settings.tokens.title', 'User Access Tokens')} + inputs={inputs} + extraInfo={extraInfo} + infoPosition='top' + server_error={this.state.serverError} + updateSection={updateSectionStatus} + width='full' + cancelButtonText={ + <FormattedMessage + id='user.settings.security.close' + defaultMessage='Close' + /> + } + /> + ); + } + + const describe = Utils.localizeMessage('user.settings.tokens.clickToEdit', "Click 'Edit' to manage your user access tokens"); + + updateSectionStatus = function updateSection() { + this.props.updateSection('tokens'); + }.bind(this); + + return ( + <SettingItemMin + title={Utils.localizeMessage('user.settings.tokens.title', 'User Access Tokens')} + describe={describe} + updateSection={updateSectionStatus} + /> + ); + } + render() { const user = this.props.user; const config = window.mm_config; @@ -959,6 +1358,11 @@ export default class SecurityTab extends React.Component { oauthSection = this.createOAuthAppsSection(); } + let tokensSection; + if (this.props.canUseAccessTokens) { + tokensSection = this.createTokensSection(); + } + return ( <div> <div className='modal-header'> @@ -1001,6 +1405,8 @@ export default class SecurityTab extends React.Component { <div className='divider-light'/> {oauthSection} <div className='divider-light'/> + {tokensSection} + <div className='divider-light'/> {signInSection} <div className='divider-dark'/> <br/> @@ -1014,7 +1420,7 @@ export default class SecurityTab extends React.Component { defaultMessage='View Access History' /> </ToggleModalButton> - <b/> + <br/> <ToggleModalButton className='security-links theme' dialogType={ActivityLogModal} @@ -1026,6 +1432,14 @@ export default class SecurityTab extends React.Component { /> </ToggleModalButton> </div> + <ConfirmModal + title={this.state.confirmTitle} + message={this.state.confirmMessage} + confirmButtonText={this.state.confirmButton} + show={this.state.showConfirmModal} + onConfirm={this.state.confirmComplete || (() => {})} //eslint-disable-line no-empty-function + onCancel={this.handleCancelConfirm} + /> </div> ); } |