diff options
Diffstat (limited to 'webapp')
-rw-r--r-- | webapp/components/admin_console/admin_sidebar.jsx | 16 | ||||
-rw-r--r-- | webapp/components/admin_console/file_upload_setting.jsx | 124 | ||||
-rw-r--r-- | webapp/components/admin_console/remove_file_setting.jsx | 72 | ||||
-rw-r--r-- | webapp/components/admin_console/saml_settings.jsx | 518 | ||||
-rw-r--r-- | webapp/components/admin_console/user_item.jsx | 4 | ||||
-rw-r--r-- | webapp/components/claim/components/email_to_oauth.jsx | 8 | ||||
-rw-r--r-- | webapp/components/claim/components/oauth_to_email.jsx | 5 | ||||
-rw-r--r-- | webapp/components/login/login_controller.jsx | 18 | ||||
-rw-r--r-- | webapp/components/signup_user_complete.jsx | 14 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_general.jsx | 28 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_security.jsx | 27 | ||||
-rw-r--r-- | webapp/i18n/en.json | 63 | ||||
-rw-r--r-- | webapp/package.json | 2 | ||||
-rw-r--r-- | webapp/routes/route_admin_console.jsx | 5 | ||||
-rw-r--r-- | webapp/sass/routes/_admin-console.scss | 6 | ||||
-rw-r--r-- | webapp/sass/routes/_signup.scss | 12 | ||||
-rw-r--r-- | webapp/utils/constants.jsx | 1 |
17 files changed, 914 insertions, 9 deletions
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx index 8f88afab4..5a31519c9 100644 --- a/webapp/components/admin_console/admin_sidebar.jsx +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -176,6 +176,7 @@ export default class AdminSidebar extends React.Component { render() { let ldapSettings = null; + let samlSettings = null; let complianceSettings = null; let license = null; @@ -198,6 +199,20 @@ export default class AdminSidebar extends React.Component { ); } + if (global.window.mm_license.SAML === 'true') { + samlSettings = ( + <AdminSidebarSection + name='saml' + title={ + <FormattedMessage + id='admin.sidebar.saml' + defaultMessage='SAML' + /> + } + /> + ); + } + if (global.window.mm_license.Compliance === 'true') { complianceSettings = ( <AdminSidebarSection @@ -391,6 +406,7 @@ export default class AdminSidebar extends React.Component { } /> {ldapSettings} + {samlSettings} </AdminSidebarSection> <AdminSidebarSection name='security' diff --git a/webapp/components/admin_console/file_upload_setting.jsx b/webapp/components/admin_console/file_upload_setting.jsx new file mode 100644 index 000000000..e7cb387ee --- /dev/null +++ b/webapp/components/admin_console/file_upload_setting.jsx @@ -0,0 +1,124 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import Setting from './setting.jsx'; + +import * as Utils from 'utils/utils.jsx'; + +export default class FileUploadSetting extends Setting { + static get propTypes() { + return { + id: React.PropTypes.string.isRequired, + label: React.PropTypes.node.isRequired, + helpText: React.PropTypes.node, + uploadingText: React.PropTypes.node, + onSubmit: React.PropTypes.func.isRequired, + disabled: React.PropTypes.bool, + fileType: React.PropTypes.string.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + fileName: null + }; + } + + handleChange() { + const files = this.refs.fileInput.files; + if (files && files.length > 0) { + this.setState({fileSelected: true, fileName: files[0].name}); + } + } + + handleSubmit(e) { + e.preventDefault(); + + $(this.refs.upload_button).button('loading'); + this.props.onSubmit(this.props.id, this.refs.fileInput.files[0], (error) => { + $(this.refs.upload_button).button('reset'); + if (error) { + Utils.clearFileInput(this.refs.fileInput); + } + this.setState({fileSelected: false, fileName: null, serverError: error}); + }); + } + + render() { + let serverError; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + var btnClass = 'btn'; + if (this.state.fileSelected) { + btnClass = 'btn btn-primary'; + } + + let fileName; + if (this.state.fileName) { + fileName = this.state.fileName; + } else { + fileName = ( + <FormattedMessage + id='admin.file_upload.noFile' + defaultMessage='No file uploaded' + /> + ); + } + + return ( + <Setting + label={this.props.label} + helpText={this.props.helpText} + inputId={this.props.id} + > + <div> + <div className='file__upload'> + <button + className='btn btn-default' + disabled={this.props.disabled} + > + <FormattedMessage + id='admin.file_upload.chooseFile' + defaultMessage='Choose File' + /> + </button> + <input + ref='fileInput' + type='file' + disabled={this.props.disabled} + accept={this.props.fileType} + onChange={this.handleChange} + /> + </div> + <button + className={btnClass} + disabled={!this.state.fileSelected} + onClick={this.handleSubmit} + ref='upload_button' + data-loading-text={`<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ${this.props.uploadingText}`} + > + <FormattedMessage + id='admin.file_upload.uploadFile' + defaultMessage='Upload' + /> + </button> + <div className='help-text no-margin'> + {fileName} + </div> + {serverError} + </div> + </Setting> + ); + } +} diff --git a/webapp/components/admin_console/remove_file_setting.jsx b/webapp/components/admin_console/remove_file_setting.jsx new file mode 100644 index 000000000..5a76faae2 --- /dev/null +++ b/webapp/components/admin_console/remove_file_setting.jsx @@ -0,0 +1,72 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import React from 'react'; + +import Setting from './setting.jsx'; + +export default class RemoveFileSetting extends Setting { + static get propTypes() { + return { + id: React.PropTypes.string.isRequired, + label: React.PropTypes.node.isRequired, + helpText: React.PropTypes.node, + removeButtonText: React.PropTypes.node.isRequired, + removingText: React.PropTypes.node, + fileName: React.PropTypes.string.isRequired, + onSubmit: React.PropTypes.func.isRequired, + disabled: React.PropTypes.bool + }; + } + + constructor(props) { + super(props); + this.handleRemove = this.handleRemove.bind(this); + + this.state = { + serverError: null + }; + } + + handleRemove(e) { + e.preventDefault(); + + $(this.refs.remove_button).button('loading'); + this.props.onSubmit(this.props.id, (error) => { + $(this.refs.remove_button).button('reset'); + this.setState({serverError: error}); + }); + } + + render() { + let serverError; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + return ( + <Setting + label={this.props.label} + helpText={this.props.helpText} + inputId={this.props.id} + > + <div> + <div className='help-text remove-filename'> + {this.props.fileName} + </div> + <button + className='btn btn-danger' + onClick={this.handleRemove} + ref='remove_button' + disabled={this.props.disabled} + data-loading-text={`<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> ${this.props.removingText}`} + > + {this.props.removeButtonText} + </button> + {serverError} + </div> + </Setting> + ); + } +}
\ No newline at end of file diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx new file mode 100644 index 000000000..db841aa83 --- /dev/null +++ b/webapp/components/admin_console/saml_settings.jsx @@ -0,0 +1,518 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import AdminSettings from './admin_settings.jsx'; +import BooleanSetting from './boolean_setting.jsx'; +import TextSetting from './text_setting.jsx'; +import FileUploadSetting from './file_upload_setting.jsx'; +import RemoveFileSetting from './remove_file_setting.jsx'; + +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import SettingsGroup from './settings_group.jsx'; + +import Client from 'utils/web_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default class SamlSettings extends AdminSettings { + constructor(props) { + super(props); + + this.getConfigFromState = this.getConfigFromState.bind(this); + + this.renderSettings = this.renderSettings.bind(this); + this.uploadCertificate = this.uploadCertificate.bind(this); + this.removeCertificate = this.removeCertificate.bind(this); + + const settings = props.config.SamlSettings; + + this.state = Object.assign(this.state, { + enable: settings.Enable, + verify: settings.Verify, + encrypt: settings.Encrypt, + idpUrl: settings.IdpUrl, + idpDescriptorUrl: settings.IdpDescriptorUrl, + assertionConsumerServiceURL: settings.AssertionConsumerServiceURL, + idpCertificateFile: settings.IdpCertificateFile, + publicCertificateFile: settings.PublicCertificateFile, + privateKeyFile: settings.PrivateKeyFile, + firstNameAttribute: settings.FirstNameAttribute, + lastNameAttribute: settings.LastNameAttribute, + emailAttribute: settings.EmailAttribute, + usernameAttribute: settings.UsernameAttribute, + nicknameAttribute: settings.NicknameAttribute, + localeAttribute: settings.LocaleAttribute, + loginButtonText: settings.LoginButtonText + }); + } + + getConfigFromState(config) { + config.SamlSettings.Enable = this.state.enable; + config.SamlSettings.Verify = this.state.verify; + config.SamlSettings.Encrypt = this.state.encrypt; + config.SamlSettings.IdpUrl = this.state.idpUrl; + config.SamlSettings.IdpDescriptorUrl = this.state.idpDescriptorUrl; + config.SamlSettings.AssertionConsumerServiceURL = this.state.assertionConsumerServiceURL; + config.SamlSettings.IdpCertificateFile = this.state.idpCertificateFile; + config.SamlSettings.PublicCertificateFile = this.state.publicCertificateFile; + config.SamlSettings.PrivateKeyFile = this.state.privateKeyFile; + config.SamlSettings.FirstNameAttribute = this.state.firstNameAttribute; + config.SamlSettings.LastNameAttribute = this.state.lastNameAttribute; + config.SamlSettings.EmailAttribute = this.state.emailAttribute; + config.SamlSettings.UsernameAttribute = this.state.usernameAttribute; + config.SamlSettings.NicknameAttribute = this.state.nicknameAttribute; + config.SamlSettings.LocaleAttribute = this.state.localeAttribute; + config.SamlSettings.LoginButtonText = this.state.loginButtonText; + + return config; + } + + uploadCertificate(id, file, callback) { + Client.uploadCertificateFile( + file, + () => { + const fileName = file.name; + this.handleChange(id, fileName); + this.setState({[id]: fileName}); + if (callback && typeof callback === 'function') { + callback(); + } + }, + (error) => { + if (callback && typeof callback === 'function') { + callback(error.message); + } + } + ); + } + + removeCertificate(id, callback) { + Client.removeCertificateFile( + this.state[id], + () => { + this.handleChange(id, ''); + this.setState({[id]: null}); + }, + (error) => { + if (callback && typeof callback === 'function') { + callback(error.message); + } + } + ); + } + + renderTitle() { + return ( + <h3> + <FormattedMessage + id='admin.authentication.saml' + defaultMessage='SAML' + /> + </h3> + ); + } + + renderSettings() { + const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true'; + if (!licenseEnabled) { + return null; + } + + let idpCert; + let privKey; + let pubCert; + + if (this.state.idpCertificateFile) { + idpCert = ( + <RemoveFileSetting + id='idpCertificateFile' + label={ + <FormattedMessage + id='admin.saml.idpCertificateFileTitle' + defaultMessage='Identity Provider Public Certificate:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.idpCertificateFileRemoveDesc' + defaultMessage='Remove the public authentication certificate issued by your Identity Provider.' + /> + } + removeButtonText={Utils.localizeMessage('admin.saml.remove.idp_certificate', 'Remove Identity Provider Certificate')} + removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')} + fileName={this.state.idpCertificateFile} + onSubmit={this.removeCertificate} + disabled={!this.state.enable} + /> + ); + } else { + idpCert = ( + <FileUploadSetting + id='idpCertificateFile' + label={ + <FormattedMessage + id='admin.saml.idpCertificateFileTitle' + defaultMessage='Identity Provider Public Certificate:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.idpCertificateFileDesc' + defaultMessage='The public authentication certificate issued by your Identity Provider.' + /> + } + uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')} + disabled={!this.state.enable} + fileType='.crt' + onSubmit={this.uploadCertificate} + /> + ); + } + + if (this.state.privateKeyFile) { + privKey = ( + <RemoveFileSetting + id='privateKeyFile' + label={ + <FormattedMessage + id='admin.saml.privateKeyFileTitle' + defaultMessage='Service Provider Private Key:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.privateKeyFileFileRemoveDesc' + defaultMessage='Remove the private key used to decrypt SAML Assertions from the Identity Provider.' + /> + } + removeButtonText={Utils.localizeMessage('admin.saml.remove.privKey', 'Remove Service Provider Private Key')} + removingText={Utils.localizeMessage('admin.saml.removing.privKey', 'Removing Private Key...')} + fileName={this.state.privateKeyFile} + onSubmit={this.removeCertificate} + disabled={!this.state.enable || !this.state.encrypt} + /> + ); + } else { + privKey = ( + <FileUploadSetting + id='privateKeyFile' + label={ + <FormattedMessage + id='admin.saml.privateKeyFileTitle' + defaultMessage='Service Provider Private Key:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.privateKeyFileFileDesc' + defaultMessage='The private key used to decrypt SAML Assertions from the Identity Provider.' + /> + } + uploadingText={Utils.localizeMessage('admin.saml.uploading.privateKey', 'Uploading Private Key...')} + disabled={!this.state.enable || !this.state.encrypt} + fileType='.key' + onSubmit={this.uploadCertificate} + /> + ); + } + + if (this.state.publicCertificateFile) { + pubCert = ( + <RemoveFileSetting + id='publicCertificateFile' + label={ + <FormattedMessage + id='admin.saml.publicCertificateFileTitle' + defaultMessage='Service Provider Public Certificate:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.publicCertificateFileRemoveDesc' + defaultMessage='Remove the certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.' + /> + } + removeButtonText={Utils.localizeMessage('admin.saml.remove.sp_certificate', 'Remove Service Provider Certificate')} + removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')} + fileName={this.state.publicCertificateFile} + onSubmit={this.removeCertificate} + disabled={!this.state.enable || !this.state.encrypt} + /> + ); + } else { + pubCert = ( + <FileUploadSetting + id='publicCertificateFile' + label={ + <FormattedMessage + id='admin.saml.publicCertificateFileTitle' + defaultMessage='Service Provider Public Certificate:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.publicCertificateFileDesc' + defaultMessage='The certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.' + /> + } + uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')} + disabled={!this.state.enable || !this.state.encrypt} + fileType='.crt' + onSubmit={this.uploadCertificate} + /> + ); + } + + return ( + <SettingsGroup> + <BooleanSetting + id='enable' + label={ + <FormattedMessage + id='admin.saml.enableTitle' + defaultMessage='Enable Login With SAML:' + /> + } + helpText={ + <FormattedHTMLMessage + id='admin.saml.enableDescription' + defaultMessage='When true, Mattermost allows login using SAML. Please see <a href="http://docs.mattermost.com/deployment/sso-saml.html" target="_blank">documentation</a> to learn more about configuring SAML for Mattermost.' + /> + } + value={this.state.enable} + onChange={this.handleChange} + /> + <TextSetting + id='idpUrl' + label={ + <FormattedMessage + id='admin.saml.idpUrlTitle' + defaultMessage='SAML SSO URL:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.idpUrlEx', 'Ex "https://idp.example.org/SAML2/SSO/Login"')} + helpText={ + <FormattedMessage + id='admin.saml.idpUrlDesc' + defaultMessage='The URL where Mattermost sends a SAML request to start login sequence.' + /> + } + value={this.state.idpUrl} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <TextSetting + id='idpDescriptorUrl' + label={ + <FormattedMessage + id='admin.saml.idpDescriptorUrlTitle' + defaultMessage='Identity Provider Issuer URL:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.idpDescriptorUrlEx', 'Ex "https://idp.example.org/SAML2/issuer"')} + helpText={ + <FormattedMessage + id='admin.saml.idpDescriptorUrlDesc' + defaultMessage='The issuer URL for the Identity Provider you use for SAML requests.' + /> + } + value={this.state.idpDescriptorUrl} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + {idpCert} + <BooleanSetting + id='verify' + label={ + <FormattedMessage + id='admin.saml.verifyTitle' + defaultMessage='Verify Signature:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.verifyDescription' + defaultMessage='When true, Mattermost verifies that the signature sent from the SAML Response matches the Service Provider Login URL' + /> + } + value={this.state.verify} + disabled={!this.state.enable} + onChange={this.handleChange} + /> + <TextSetting + id='assertionConsumerServiceURL' + label={ + <FormattedMessage + id='admin.saml.assertionConsumerServiceURLTitle' + defaultMessage='Service Provider Login URL:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.assertionConsumerServiceURLEx', 'Ex "https://<your-mattermost-url>/login/sso/saml"')} + helpText={ + <FormattedMessage + id='admin.saml.assertionConsumerServiceURLDesc' + defaultMessage='Enter https://<your-mattermost-url>/login/sso/saml. Make sure you use HTTP or HTTPS in your URL depending on your server configuration. This field is also known as the Assertion Consumer Service URL.' + /> + } + value={this.state.assertionConsumerServiceURL} + onChange={this.handleChange} + disabled={!this.state.enable || !this.state.verify} + /> + <BooleanSetting + id='encrypt' + label={ + <FormattedMessage + id='admin.saml.encryptTitle' + defaultMessage='Enable Encryption:' + /> + } + helpText={ + <FormattedMessage + id='admin.saml.encryptDescription' + defaultMessage='When true, Mattermost will decrypt SAML Assertions encrypted with your Service Provider Public Certificate.' + /> + } + value={this.state.encrypt} + disabled={!this.state.enable} + onChange={this.handleChange} + /> + {privKey} + {pubCert} + <TextSetting + id='emailAttribute' + label={ + <FormattedMessage + id='admin.saml.emailAttrTitle' + defaultMessage='Email Attribute:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.emailAttrEx', 'Ex "Email" or "PrimaryEmail"')} + helpText={ + <FormattedMessage + id='admin.saml.emailAttrDesc' + defaultMessage='The attribute in the SAML Assertion that will be used to populate the email addresses of users in Mattermost.' + /> + } + value={this.state.emailAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <TextSetting + id='usernameAttribute' + label={ + <FormattedMessage + id='admin.saml.usernameAttrTitle' + defaultMessage='Username Attribute:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.usernameAttrEx', 'Ex "Username"')} + helpText={ + <FormattedMessage + id='admin.saml.usernameAttrDesc' + defaultMessage='The attribute in the SAML Assertion that will be used to populate the username field in Mattermost.' + /> + } + value={this.state.usernameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <TextSetting + id='firstNameAttribute' + label={ + <FormattedMessage + id='admin.saml.firstnameAttrTitle' + defaultMessage='First Name Attribute:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.firstnameAttrEx', 'Ex "FirstName"')} + helpText={ + <FormattedMessage + id='admin.saml.firstnameAttrDesc' + defaultMessage='The attribute in the SAML Assertion that will be used to populate the first name of users in Mattermost.' + /> + } + value={this.state.firstNameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <TextSetting + id='lastNameAttribute' + label={ + <FormattedMessage + id='admin.saml.lastnameAttrTitle' + defaultMessage='Last Name Attribute:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.lastnameAttrEx', 'Ex "LastName"')} + helpText={ + <FormattedMessage + id='admin.saml.lastnameAttrDesc' + defaultMessage='The attribute in the SAML Assertion that will be used to populate the last name of users in Mattermost.' + /> + } + value={this.state.lastNameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <TextSetting + id='nicknameAttribute' + label={ + <FormattedMessage + id='admin.saml.nicknameAttrTitle' + defaultMessage='Nickname Attribute:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.nicknameAttrEx', 'Ex "Nickname"')} + helpText={ + <FormattedMessage + id='admin.saml.nicknameAttrDesc' + defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.' + /> + } + value={this.state.nicknameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <TextSetting + id='localeAttribute' + label={ + <FormattedMessage + id='admin.saml.localeAttrTitle' + defaultMessage='Preferred Language Attribute:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.localeAttrEx', 'Ex "Locale" or "PrimaryLanguage"')} + helpText={ + <FormattedMessage + id='admin.saml.localeAttrDesc' + defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the language of users in Mattermost.' + /> + } + value={this.state.localeAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <TextSetting + id='loginButtonText' + label={ + <FormattedMessage + id='admin.saml.loginButtonTextTitle' + defaultMessage='Login Button Text:' + /> + } + placeholder={Utils.localizeMessage('admin.saml.loginButtonTextEx', 'Ex "With OKTA"')} + helpText={ + <FormattedMessage + id='admin.saml.loginButtonTextDesc' + defaultMessage='(Optional) The text that appears in the login button on the login page. Defaults to "With SAML".' + /> + } + value={this.state.loginButtonText} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + </SettingsGroup> + ); + } +}
\ No newline at end of file diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx index 62de50f0f..edded5aab 100644 --- a/webapp/components/admin_console/user_item.jsx +++ b/webapp/components/admin_console/user_item.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import Client from 'utils/web_client.jsx'; +import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; import UserStore from 'stores/user_store.jsx'; import ConfirmModal from '../confirm_modal.jsx'; @@ -374,12 +375,13 @@ export default class UserItem extends React.Component { let authServiceText; let passwordReset; if (user.auth_service) { + const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service); authServiceText = ( <FormattedHTMLMessage id='admin.user_item.authServiceNotEmail' defaultMessage=', <strong>Sign-in Method:</strong> {service}' values={{ - service: Utils.toTitleCase(user.auth_service) + service }} /> ); diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx index 6b0a90e8e..422b31a3a 100644 --- a/webapp/components/claim/components/email_to_oauth.jsx +++ b/webapp/components/claim/components/email_to_oauth.jsx @@ -3,6 +3,7 @@ import * as Utils from 'utils/utils.jsx'; import Client from 'utils/web_client.jsx'; +import Constants from 'utils/constants.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -55,7 +56,8 @@ export default class EmailToOAuth extends React.Component { formClass += ' has-error'; } - const uiType = Utils.toTitleCase(this.props.newType) + ' SSO'; + const type = (this.props.newType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.newType)); + const uiType = `${type} SSO`; return ( <div> @@ -74,7 +76,7 @@ export default class EmailToOAuth extends React.Component { id='claim.email_to_oauth.ssoType' defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO' values={{ - type: Utils.toTitleCase(this.props.newType) + type }} /> </p> @@ -83,7 +85,7 @@ export default class EmailToOAuth extends React.Component { id='claim.email_to_oauth.ssoNote' defaultMessage='You must already have a valid {type} account' values={{ - type: Utils.toTitleCase(this.props.newType) + type }} /> </p> diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx index 17ca12264..6a0f6431b 100644 --- a/webapp/components/claim/components/oauth_to_email.jsx +++ b/webapp/components/claim/components/oauth_to_email.jsx @@ -3,6 +3,7 @@ import * as Utils from 'utils/utils.jsx'; import Client from 'utils/web_client.jsx'; +import Constants from 'utils/constants.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -62,7 +63,7 @@ export default class OAuthToEmail extends React.Component { formClass += ' has-error'; } - const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO'; + const uiType = `${(this.props.currentType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.currentType))} SSO`; return ( <div> @@ -85,7 +86,7 @@ export default class OAuthToEmail extends React.Component { <p> <FormattedMessage id='claim.oauth_to_email.enterNewPwd' - defaultMessage='Enter a new password for your {site} account' + defaultMessage='Enter a new password for your {site} email account' values={{ site: global.window.mm_config.SiteName }} diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx index 653908654..cd4175d3c 100644 --- a/webapp/components/login/login_controller.jsx +++ b/webapp/components/login/login_controller.jsx @@ -43,6 +43,7 @@ export default class LoginController extends React.Component { ldapEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableLdap === 'true', usernameSigninEnabled: global.window.mm_config.EnableSignInWithUsername === 'true', emailSigninEnabled: global.window.mm_config.EnableSignInWithEmail === 'true', + samlEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableSaml === 'true', loginId: '', // the browser will set a default for this password: '', showMfa: false @@ -319,6 +320,7 @@ export default class LoginController extends React.Component { const ldapEnabled = this.state.ldapEnabled; const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true'; const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true'; + const samlSigninEnabled = this.state.samlEnabled; const usernameSigninEnabled = this.state.usernameSigninEnabled; const emailSigninEnabled = this.state.emailSigninEnabled; @@ -416,7 +418,7 @@ export default class LoginController extends React.Component { ); } - if ((emailSigninEnabled || usernameSigninEnabled || ldapEnabled) && (gitlabSigninEnabled || googleSigninEnabled)) { + if ((emailSigninEnabled || usernameSigninEnabled || ldapEnabled) && (gitlabSigninEnabled || googleSigninEnabled || samlSigninEnabled)) { loginControls.push( <div key='divider' @@ -475,6 +477,20 @@ export default class LoginController extends React.Component { ); } + if (samlSigninEnabled) { + loginControls.push( + <a + className='btn btn-custom-login saml' + key='gitlab' + href={'/login/sso/saml' + this.props.location.search} + > + <span> + {window.mm_config.SamlLoginButtonText} + </span> + </a> + ); + } + return ( <div> {extraBox} diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx index c7ddfc91b..fa5e9268e 100644 --- a/webapp/components/signup_user_complete.jsx +++ b/webapp/components/signup_user_complete.jsx @@ -588,6 +588,20 @@ export default class SignupUserComplete extends React.Component { ); } + if (global.window.mm_config.EnableSaml === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true') { + signupMessage.push( + <a + className='btn btn-custom-login saml' + key='saml' + href={`/login/sso/saml${window.location.search}${window.location.search ? '&' : '?'}action=signup`} + > + <span> + {global.window.mm_config.SamlLoginButtonText} + </span> + </a> + ); + } + let ldapSignup; if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) { ldapSignup = ( diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx index f8910b9bc..5e821a26a 100644 --- a/webapp/components/user_settings/user_settings_general.jsx +++ b/webapp/components/user_settings/user_settings_general.jsx @@ -412,6 +412,24 @@ class UserSettingsGeneralTab extends React.Component { {helpText} </div> ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + inputs.push( + <div + key='oauthEmailInfo' + className='form-group' + > + <div className='setting-list__hint'> + <FormattedMessage + id='user.settings.general.emailSamlCantUpdate' + defaultMessage='Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.' + values={{ + email: this.state.email + }} + /> + </div> + {helpText} + </div> + ); } emailSection = ( @@ -478,6 +496,16 @@ class UserSettingsGeneralTab extends React.Component { }} /> ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + describe = ( + <FormattedMessage + id='user.settings.general.loginSaml' + defaultMessage='Login done through SAML ({email})' + values={{ + email: this.state.email + }} + /> + ); } emailSection = ( diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index af7aeb3c6..247dc0f81 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -620,6 +620,24 @@ class SecurityTab extends React.Component { ); } + let samlOption; + if (global.window.mm_config.EnableSaml === 'true' && user.auth_service === '') { + samlOption = ( + <div> + <Link + className='btn btn-primary' + to={'/claim/email_to_oauth?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.SAML_SERVICE} + > + <FormattedMessage + id='user.settings.security.switchSaml' + defaultMessage='Switch to using SAML SSO' + /> + </Link> + <br/> + </div> + ); + } + const inputs = []; inputs.push( <div key='userSignInOption'> @@ -627,6 +645,7 @@ class SecurityTab extends React.Component { {gitlabOption} <br/> {ldapOption} + {samlOption} {googleOption} </div> ); @@ -681,6 +700,13 @@ class SecurityTab extends React.Component { defaultMessage='LDAP' /> ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + describe = ( + <FormattedMessage + id='user.settings.security.saml' + defaultMessage='SAML' + /> + ); } return ( @@ -701,6 +727,7 @@ class SecurityTab extends React.Component { numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods; + numMethods = global.window.mm_config.EnableSaml === 'true' ? numMethods + 1 : numMethods; let signInSection; if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) { diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 02d11e484..26b1b47fd 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -102,6 +102,7 @@ "admin.audits.title": "User Activity Logs", "admin.authentication.email": "Email Auth", "admin.authentication.gitlab": "GitLab", + "admin.authentication.saml": "SAML", "admin.banner.heading": "Note:", "admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.", "admin.compliance.directoryExample": "Ex \"./data/\"", @@ -217,6 +218,9 @@ "admin.email.smtpUsernameTitle": "SMTP Server Username:", "admin.email.testing": "Testing...", "admin.false": "false", + "admin.file_upload.chooseFile": "Choose File", + "admin.file_upload.noFile": "No file uploaded", + "admin.file_upload.uploadFile": "Upload", "admin.files.images": "Images", "admin.files.storage": "Storage", "admin.general.configuration": "Configuration", @@ -431,6 +435,58 @@ "admin.reset_password.submit": "Please enter at least {chars} characters.", "admin.reset_password.titleReset": "Reset Password", "admin.reset_password.titleSwitch": "Switch Account to Email/Password", + "admin.saml.assertionConsumerServiceURLDesc": "Enter https://<your-mattermost-url>/login/sso/saml. Make sure you use HTTP or HTTPS in your URL depending on your server configuration. This field is also known as the Assertion Consumer Service URL.", + "admin.saml.assertionConsumerServiceURLEx": "Ex \"https://<your-mattermost-url>/login/sso/saml\"", + "admin.saml.assertionConsumerServiceURLTitle": "Service Provider Login URL:", + "admin.saml.emailAttrDesc": "The attribute in the SAML Assertion that will be used to populate the email addresses of users in Mattermost.", + "admin.saml.emailAttrEx": "Ex \"Email\" or \"PrimaryEmail\"", + "admin.saml.emailAttrTitle": "Email Attribute:", + "admin.saml.enableDescription": "When true, Mattermost allows login using SAML. Please see <a href='http://docs.mattermost.com/deployment/sso-saml.html' target='_blank'>documentation</a> to learn more about configuring SAML for Mattermost.", + "admin.saml.enableTitle": "Enable Login With SAML:", + "admin.saml.encryptDescription": "When true, Mattermost will decrypt SAML Assertions encrypted with your Service Provider Public Certificate.", + "admin.saml.encryptTitle": "Enable Encryption:", + "admin.saml.firstnameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the first name of users in Mattermost.", + "admin.saml.firstnameAttrEx": "Ex \"FirstName\"", + "admin.saml.firstnameAttrTitle": "First Name Attribute:", + "admin.saml.idpCertificateFileDesc": "The public authentication certificate issued by your Identity Provider.", + "admin.saml.idpCertificateFileRemoveDesc": "Remove the public authentication certificate issued by your Identity Provider.", + "admin.saml.idpCertificateFileTitle": "Identity Provider Public Certificate:", + "admin.saml.idpDescriptorUrlDesc": "The issuer URL for the Identity Provider you use for SAML requests.", + "admin.saml.idpDescriptorUrlEx": "Ex \"https://idp.example.org/SAML2/issuer\"", + "admin.saml.idpDescriptorUrlTitle": "Identity Provider Issuer URL:", + "admin.saml.idpUrlDesc": "The URL where Mattermost sends a SAML request to start login sequence.", + "admin.saml.idpUrlEx": "Ex \"https://idp.example.org/SAML2/SSO/Login\"", + "admin.saml.idpUrlTitle": "SAML SSO URL:", + "admin.saml.lastnameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the last name of users in Mattermost.", + "admin.saml.lastnameAttrEx": "Ex \"LastName\"", + "admin.saml.lastnameAttrTitle": "Last Name Attribute:", + "admin.saml.localeAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the language of users in Mattermost.", + "admin.saml.localeAttrEx": "Ex \"Locale\" or \"PrimaryLanguage\"", + "admin.saml.localeAttrTitle": "Preferred Language Attribute:", + "admin.saml.loginButtonTextDesc": "(Optional) The text that appears in the login button on the login page. Defaults to \"With SAML\".", + "admin.saml.loginButtonTextEx": "Ex \"With OKTA\"", + "admin.saml.loginButtonTextTitle": "Login Button Text:", + "admin.saml.nicknameAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.", + "admin.saml.nicknameAttrEx": "Ex \"Nickname\"", + "admin.saml.nicknameAttrTitle": "Nickname Attribute:", + "admin.saml.privateKeyFileFileDesc": "The private key used to decrypt SAML Assertions from the Identity Provider.", + "admin.saml.privateKeyFileFileRemoveDesc": "Remove the private key used to decrypt SAML Assertions from the Identity Provider.", + "admin.saml.privateKeyFileTitle": "Service Provider Private Key:", + "admin.saml.publicCertificateFileDesc": "The certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.", + "admin.saml.publicCertificateFileRemoveDesc": "Remove the certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.", + "admin.saml.publicCertificateFileTitle": "Service Provider Public Certificate:", + "admin.saml.remove.idp_certificate": "Remove Identity Provider Certificate", + "admin.saml.remove.privKey": "Remove Service Provider Private Key", + "admin.saml.remove.sp_certificate": "Remove Service Provider Certificate", + "admin.saml.removing.certificate": "Removing Certificate...", + "admin.saml.removing.privKey": "Removing Private Key...", + "admin.saml.uploading.certificate": "Uploading Certificate...", + "admin.saml.uploading.privateKey": "Uploading Private Key...", + "admin.saml.usernameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the username field in Mattermost.", + "admin.saml.usernameAttrEx": "Ex \"Username\"", + "admin.saml.usernameAttrTitle": "Username Attribute:", + "admin.saml.verifyDescription": "When true, Mattermost verifies that the signature sent from the SAML Response matches the Service Provider Login URL", + "admin.saml.verifyTitle": "Verify Signature:", "admin.save": "Save", "admin.saving": "Saving Config...", "admin.security.connection": "Connections", @@ -522,6 +578,7 @@ "admin.sidebar.rateLimiting": "Rate Limiting", "admin.sidebar.reports": "REPORTING", "admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu", + "admin.sidebar.saml": "SAML", "admin.sidebar.security": "Security", "admin.sidebar.sessions": "Sessions", "admin.sidebar.settings": "SETTINGS", @@ -842,7 +899,7 @@ "claim.ldap_to_email.title": "Switch LDAP Account to Email/Password", "claim.oauth_to_email.confirm": "Confirm Password", "claim.oauth_to_email.description": "Upon changing your account type, you will only be able to login with your email and password.", - "claim.oauth_to_email.enterNewPwd": "Enter a new password for your {site} account", + "claim.oauth_to_email.enterNewPwd": "Enter a new password for your {site} email account", "claim.oauth_to_email.enterPwd": "Please enter a password.", "claim.oauth_to_email.newPwd": "New Password", "claim.oauth_to_email.pwdNotMatch": "Password do not match.", @@ -1454,6 +1511,7 @@ "user.settings.general.emailHelp4": "A verification email was sent to {email}.", "user.settings.general.emailLdapCantUpdate": "Login occurs through LDAP. Email cannot be updated. Email address used for notifications is {email}.", "user.settings.general.emailMatch": "The new emails you entered do not match.", + "user.settings.general.emailSamlCantUpdate": "Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.", "user.settings.general.emailUnchanged": "Your new email address is the same as your old email address.", "user.settings.general.emptyName": "Click 'Edit' to add your full name", "user.settings.general.emptyNickname": "Click 'Edit' to add a nickname", @@ -1465,6 +1523,7 @@ "user.settings.general.lastName": "Last Name", "user.settings.general.loginGitlab": "Login done through GitLab ({email})", "user.settings.general.loginLdap": "Login done through LDAP ({email})", + "user.settings.general.loginSaml": "Login done through SAML ({email})", "user.settings.general.newAddress": "New Address: {email}<br />Check your email to verify the above address.", "user.settings.general.nickname": "Nickname", "user.settings.general.nicknameExtra": "Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.", @@ -1552,10 +1611,12 @@ "user.settings.security.passwordLengthError": "New passwords must be at least {chars} characters", "user.settings.security.passwordMatchError": "The new passwords you entered do not match", "user.settings.security.retypePassword": "Retype New Password", + "user.settings.security.saml": "SAML", "user.settings.security.switchEmail": "Switch to using email and password", "user.settings.security.switchGitlab": "Switch to using GitLab SSO", "user.settings.security.switchGoogle": "Switch to using Google SSO", "user.settings.security.switchLdap": "Switch to using LDAP", + "user.settings.security.switchSaml": "Switch to using SAML SSO", "user.settings.security.title": "Security Settings", "user.settings.security.viewHistory": "View Access History", "user_list.notFound": "No users found", diff --git a/webapp/package.json b/webapp/package.json index 69d91e345..62e8b0bc1 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,7 +18,7 @@ "keymirror": "0.1.1", "marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b", "match-at": "0.1.0", - "mattermost": "mattermost/mattermost-javascript#8e4c320d5af653eacb248455d77057a76ec28830", + "mattermost": "mattermost/mattermost-javascript#798c39c5d302d2d109e768a35575ebdbf2a8ee6a", "object-assign": "4.1.0", "perfect-scrollbar": "0.6.11", "react": "15.0.2", diff --git a/webapp/routes/route_admin_console.jsx b/webapp/routes/route_admin_console.jsx index b088b430b..1f5e69c2d 100644 --- a/webapp/routes/route_admin_console.jsx +++ b/webapp/routes/route_admin_console.jsx @@ -15,6 +15,7 @@ import LogSettings from 'components/admin_console/log_settings.jsx'; import EmailAuthenticationSettings from 'components/admin_console/email_authentication_settings.jsx'; import GitLabSettings from 'components/admin_console/gitlab_settings.jsx'; import LdapSettings from 'components/admin_console/ldap_settings.jsx'; +import SamlSettings from 'components/admin_console/saml_settings.jsx'; import SignupSettings from 'components/admin_console/signup_settings.jsx'; import LoginSettings from 'components/admin_console/login_settings.jsx'; import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx'; @@ -90,6 +91,10 @@ export default ( path='ldap' component={LdapSettings} /> + <Route + path='saml' + component={SamlSettings} + /> </Route> <Route path='security'> <IndexRedirect to='sign_up'/> diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index 4dba1558c..a1b2d772d 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -132,6 +132,12 @@ .btn { font-size: 13px; } + + &.remove-filename { + margin-bottom: 5px; + top: -2px; + position: relative; + } } .alert { diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss index 4dc0dce42..804e4c890 100644 --- a/webapp/sass/routes/_signup.scss +++ b/webapp/sass/routes/_signup.scss @@ -280,6 +280,18 @@ } } + &.saml { + background: #dd4b39; + + &:hover { + background: darken(#dd4b39, 10%); + } + + span { + vertical-align: middle; + } + } + &.btn-full { padding-left: 35px; text-align: left; diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 0934e8de9..1b0fa6374 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -227,6 +227,7 @@ export default { GOOGLE_SERVICE: 'google', EMAIL_SERVICE: 'email', LDAP_SERVICE: 'ldap', + SAML_SERVICE: 'saml', USERNAME_SERVICE: 'username', SIGNIN_CHANGE: 'signin_change', PASSWORD_CHANGE: 'password_change', |